혼자 울면서

1. Overview & IDEA - PPT Searching 2. Extracting with PPT 3. TF-IDF with PPT 4. Threading with PPT 5. Operating with PPT

TF-IDF

내가 이해하는 TF-IDF 알고리즘은 아래와 같다.

  • TF(Term Frequency) : 단일 문서 내에서 특정 키워드의 등장 빈도
  • IDF(Inverse Document Frequency) : 문서군 내에서 특정 키워드가 등장한 문서의 개수의 역수
  • TF-IDF : TF와 IDF를 곱하게 되면 문서군내에 해당 문서에서 각 키워드가 얼마나 중요한지를 나타내는 값을 얻을 수 있다.

이를 통해서 우선 특정 폴더를 대상으로 추출을 한뒤 비교 분석을 조금 해보았다. 사실 TF-IDF를 직접 구현 하려고 했지만 시간상 sklearn을 사용했다. 다만 추후에 설명할 수 있겠지만 TF-IDF모듈을 잘 뜯어보면 CountVectorizer()와 같은 객체를 통해서 실제 갯수를 볼 수도 있다. 이를 통해 운영 단계에서 최적화를 할예정이 있었다. (하지만 선임의 반대로 구현까지 할 수 는 없었다는건 안비밀… ㅠㅠ)

def tfidf_cal_func(folder_path="./", min=2, output_name="output", ngram=None):
    extract_text_df = pd.DataFrame(
        list(PRE.folder2data(folder_path).items()), columns=["location", "keyword"]
    )
    keyword = extract_text_df["keyword"].tolist()
    location = extract_text_df["location"].tolist()
    if ngram is None:
        tfidf_vectorizer = TfidfVectorizer(min_df=min)
    else:
        tfidf_vectorizer = TfidfVectorizer(min_df=min, ngram_range=ngram)
 
	# 후처리 코드 
    tfidf_vectorizer.fit(keyword)
    # TfidfVectorizer에 vocabulary라는 코드가 전체 키워드를 리턴해준다.
    word_id_list = sorted(
        tfidf_vectorizer.vocabulary_.items(), key=lambda x: x[1], reverse=False
    )
    word_list = [x[0] for x in word_id_list]
 
    tf_idf_df = pd.DataFrame(
        tfidf_vectorizer.transform(keyword).toarray(), columns=word_list
    )
    # reverse는 키워드를 row로 가져오기 위한 방법이었고 해당 키워드의 문서군내 weight를 적어 주었다.
    reverse_tf_idf_df = tf_idf_df.transpose()
    text_ranking_dict = {}
    for num in range(0, len(location)):
        temp = reverse_tf_idf_df[num].sort_values(ascending=False)
        temp_index_list = temp.index.tolist()
        text_ranking_dict[location[num]] = temp_index_list
        text_ranking_dict[location[num] + " weight"] = temp.tolist()
    result_data = pd.DataFrame(text_ranking_dict)
    result_data.to_csv("./" + output_name + ".csv", encoding="utf-8-sig")
    return result_data
 

이부분이 TF-IDF를 계산하고 적절한 후처리를 통해서 엑셀에 row : 문서, column : 단어를 낸 결과이다.

  1. folder2data함수를 이용해 DataFrame 추출해낸다.
  2. 이후 N-gram을 통해서 정확성을 조금 더 올리려고 한 부분이 있다.
    1. 당시 시간이 남은게 아니라 특정 단어들에서는 2개의 단어를 이어 붙였을 때 성능이 조금 더 좋아져서 해당 option을 붙인 기억이 있다.
  3. 이렇게 한뒤 적절한 후처리를 거쳐서 csv로 뽑아낸다.

이러한 결과물에 사진이 없는 이유는 보안 때문이다. 해당 프로젝트 들이 회사에 있는 전체 데이터를 가지고 진행되었고 해당 데이터들은 내부 기밀이기 때문에 들고 나올 수 없다

그리고 아래는 조금 다른 코드인데 main함수를 만들어서 sys변수를 만들고 이에 따라서 프로그램을 구동할 수 있고 help를 만들어서 지속적으로 사용할 수 있게 프로그래밍했다.

explain_txt = """
This program will extract keyword from text
there are four options
    1. folder_path 
        COMMAND INPUT : -i [folder_path]
        DEFAULT VALUE : ./
        input is set target folder extract keyword from "only" pptx
        program run on recursivly track the folder and fun all child folder
    2. min : -m [minimun number about existing keyword]
        COMMAND INPUT : -m [minimun number]
        DEFAULT VALUE : 2
        About keyword this will take the threshold for keyword Count
    3. output_name
        COMMAND INPUT : -o [output_path]
        DEFAULT VALUE : output
        output_name will run for excel
    4. ngram
        COMMAND INPUT : -n [ngram max number]
        DEFAULT VALUE : None
        if you want to take the ngram algorithm in tfidf this will be parameter for ngram"""
 
 
if __name__ == "__main__":
    print(sys.argv)
    if ("-h" in sys.argv or "-help" in sys.argv or "h" in sys.argv) and len(
        sys.argv
    ) == 2:
        print(explain_txt)
    else:
        input_name = "./"
        min_val = 2
        output_name = "output"
        ngram = None
        if "-i" in sys.argv:
            input_name = sys.argv[sys.argv.index("-i") + 1]
        if "-o" in sys.argv:
            output_name = sys.argv[sys.argv.index("-o") + 1]
        if "-m" in sys.argv:
            min_val = sys.argv[sys.argv.index("-m") + 1]
        if "-n" in sys.argv:
            ngram = (1, int(sys.argv[sys.argv.index("-m") + 1]))
        start = time.time()
        print("*" * 50, "root folder {} extrating start".format(input_name), "*" * 50)
        tfidf_cal_func(
            folder_path=input_name,
            min=int(min_val),
            output_name=output_name,
            ngram=ngram,
        )
        print("*" * 50, "root folder {} extrating end".format(input_name), "*" * 50)
        end = time.time()
        print("total extrating time : {} sec".format(end - start))
 

Result

결과를 보기에 앞서서 프로그램 구동시 고려해야할 점에 대해서 언급해보자

앞서서 이야기 했듯이 건축과 건축이외의 공정에 어느정도 연관이 있는 상황이다. 이를 TFIDF에 적용을 하기 위해서는 2가지가 고려되야한다.

  1. 어디까지를 문서군으로 정의할 것인가?
  2. 공종간에 관계가 실제로 유의미하게 차이가 나는 것인지?

위의 2가지는 성능에서 유의미한 차이를 보일 수 있는 가능성이 존재한다. 해서 총 2가지 실험을 진행했다.

대분류 vs 소분류

프로젝트가 아니라 쌓여있는 DB를 기준으로 보았을 때 폴더 트리는 그 깊이가 깊을 수도 깊지 않을 수도 있다. 하지만 TFIDF는 분명 문서군이라는 걸 기준으로 그 역수값을 계산한다. 해서 가중치를 계산하는 집단의 크기가 분명 중요해 보였다.

문서에서 이름조차 정확하게 보여줄 수 없는 상황이지만 어쨌든 비슷한 단어들에 대해서 문서군이 좀더 큰 상황에서 분명 유의미하게 가중치의 차이가 있는 것으로 보여진다. 하지만 크게 차이가 없는 것 또한 볼 수 있다.

공종별 문서 비교

이 부분이 설명하기가 좀 어려운 부분인데 이런 가설이 있다고 생각하면 좋을 것 같다.

건축은 범용적으로 사용하는 언어들이 많기 때문에 특정 건축용어에 대해서는 가중치가 유의미 하지 않을 수 있다. 하지만 건축 이외의 공종은 좀 더 전문적인 내용이 문서에 담기면서 좀 더 구분하기가 쉬워질 것이다. 해서 나온 결과는 아래와 같다.

위에 그래프 처럼 공종이 특정된 문서에서 각기 다른 단어들에 대한 대분류 체계에서의 가중치 값은 유의미 하게 높은 것으로 보여진다.

이를 통해서 내릴 수 있는 결론은 아래와 같다.

소분류보다는 대분류가 더 의미 있는 문서군이며 특히 같은 문서군 내에서도 건축에 관련된 문서보다는 특정 공종에 관련된 문서군이 더 유의미한 차이를 가진다.

여기까지의 결론으로 선임분을 설득 시킬 수 있었다. 각 문서군과 공종에 따라서 분류를 어느정도 다시 진행해야하고 이를 통해서 나온 가중치 값으로 문서를 정렬해야한다! (선임님은 설계를 오래하신 분이지만 높은 인사이트로 프로젝트를 보고 계셨다. 코드는 아예 모르셔도 인사이트가 대단했던 것 같다.)

->다음편4. Threading with PPT에서 계속

1번 그림 차트

type: bar
labels: [wordA-0, wordA-1, wordA-2, wordA-3]
series:
  - title: 대분류
    data: [0.28,0.121,0.123,0.11]
  - title: 소분류
    data: [0.25,0.117,0.116,0.122]
tension: 0.47
width: 80%
labelColors: false
fill: false
beginAtZero: true
bestFit: false
bestFitTitle: undefined
bestFitNumber: 0

2번 그림 차트

type: bar
labels: [wordA, wordB, wordC, wordD]
series:
  - title: 대분류
    data: [0.39, 0.25,0.21,0.2]
  - title: 소분류
    data: [0.25,0.21,0.11,0.13]
tension: 0.2
width: 80%
labelColors: false
fill: false
beginAtZero: true
bestFit: false
bestFitTitle: undefined
bestFitNumber: 0