對盜圖、盜文、盜墓深惡痛絕嗎?PostgreSQL結合餘弦、線性相關演算法 在文字、圖片、陣列相似 等領域的應用 - 1 理論 - tf/idf...

weixin_33766168發表於2017-01-17

標籤

PostgreSQL , 文字分析 , tf , idf , tf-idf , tag


背景

很多網站有標籤的功能,會根據網頁自動生成標籤,標籤實際上就是該網頁的關鍵詞,比如一個賣手機的網頁,那麼標籤是如何生成的呢?

pic

在一篇文件裡面,是不是出現越多的詞,就越是關鍵詞呢?

比如在中文裡面的、是、我、你可能出現次數是比較多的,它們很顯然不是關鍵詞,這些屬於stop word,是需要被忽略的。

另外還有一些詞,雖然不是stop word,但是也可能不算關鍵詞,比如應用、科學、普通話,等等一些在很多文章中都可能出現的詞,可能不是關鍵詞。

有什麼好的演算法能提取文章的關鍵詞呢?

TF-IDF演算法

TF-IDF是一種統計方法,用以評估一字詞對於一個檔案集或一個語料庫中的其中一份檔案的重要程度。字詞的重要性隨著它在檔案中出現的次數成正比增加,但同時會隨著它在語料庫中出現的頻率成反比下降。

TF-IDF加權的各種形式常被搜尋引擎應用,作為檔案與使用者查詢之間相關程度的度量或評級。除了TF-IDF以外,因特網上的搜尋引擎還會使用基於連結分析的評級方法,以確定檔案在搜尋結果中出現的順序。

1. tf比較好理解,就是一個詞在當前文字的出現頻率,後面會有例子。

2. idf是一個加權,逆向檔案頻率(inverse document frequency,IDF)是一個詞語普遍重要性的度量。也就是說一些普通詞,因此越普通的詞,它的IDF越低。

某一特定詞語的IDF,可以由總檔案數目除以包含該詞語之檔案的數目,再取對數得到IDF。

3. 計算一個詞在文件中的關鍵性,計算TF與IDF的乘積即得到。因此,TF-IDF傾向於過濾掉常見的詞語,保留重要的詞語。

舉例

例1

有很多不同的數學公式可以用來計算TF-IDF。

這邊的例子以上述的數學公式來計算。

1. 詞頻 (TF) 是一詞語出現的次數除以該檔案的總詞語數。

假如一篇檔案的總詞語數是100個,而詞語“母牛”出現了3次,那麼“母牛”一詞在該檔案中的詞頻就是3/100=0.03。

2. 一個計算檔案頻率 (IDF) 的方法是檔案集裡包含的檔案總數除以測定有多少份檔案出現過“母牛”一詞。

所以,如果“母牛”一詞在1,000份檔案出現過,而檔案總數是10,000,000份的話,其逆向檔案頻率就是 log(10,000,000 / 1,000)=4。

3. 最後“母牛”的TF-IDF的分數為0.03 * 4=0.12。

TF-IDF越高,說明這個詞在該文字中越關鍵。

例2

在某個一共有一千詞的網頁中“原子能”、“的”和“應用”分別出現了 2 次、35 次 和 5 次,那麼它們的詞頻就分別是 0.002、0.035 和 0.005。

我們將這三個數相加,其和 0.042 就是相應網頁和查詢“原子能的應用” 相關性的一個簡單的度量。

概括地講,如果一個查詢包含關鍵詞 w1,w2,...,wN, 它們在一篇特定網頁中的詞頻分別是: TF1, TF2, ..., TFN。 (TF: term frequency)。 那麼,這個查詢和該網頁的相關性就是:TF1 + TF2 + ... + TFN。

讀者可能已經發現了又一個漏洞。

在上面的例子中,詞“的”佔了總詞頻的 80% 以上,而它對確定網頁的主題幾乎沒有用。我們稱這種詞叫“應刪除詞”(Stopwords),也就是說在度量相關性是不應考慮它們的頻率。在漢語中,應刪除詞還有“是”、“和”、“中”、“地”、“得”等等幾十個。

忽略這些應刪除詞後,上述網頁的相似度就變成了0.007,其中“原子能”貢獻了 0.002,“應用”貢獻了 0.005。

細心的讀者可能還會發現另一個小的漏洞。

在漢語中,“應用”是個很通用的詞,而“原子能”是個很專業的詞,後者在相關性排名中比前者重要。因此我們需要給漢語中的每一個詞給一個權重(IDF),這個權重的設定必須滿足下面兩個條件:

1. 一個詞預測主題能力越強,權重就越大,反之,權重就越小。我們在網頁中看到“原子能”這個詞,或多或少地能瞭解網頁的主題。我們看到“應用”一次,對主題基本上還是一無所知。因此,“原子能“的權重就應該比應用大。

2. 應刪除詞的權重應該是零。

我們很容易發現,如果一個關鍵詞只在很少的網頁中出現,我們通過它就容易鎖定搜尋目標,它的權重也就應該大。

反之如果一個詞在大量網頁中出現,我們看到它仍然不是很清楚要找什麼內容,因此它應該小。

概括地講,假定一個關鍵詞 w 在 Dw 個網頁中出現過,那麼 Dw 越大,w的權重越小,反之亦然。

在資訊檢索中,使用最多的權重是“逆文字頻率指數” (Inverse document frequency 縮寫為IDF),它的公式為log(D/Dw)其中D是全部網頁數。

比如,我們假定中文網頁數是D=10億,應刪除詞“的”在所有的網頁中都出現,即Dw=10億,那麼它的IDF=log(10億/10億)= log (1) = 0。

假如專用詞“原子能”在兩百萬個網頁中出現,即Dw=200萬,則它的權重IDF=log(500) =2.7。

又假定通用詞“應用”,出現在五億個網頁中,它的權重IDF = log(2)則只有 0.3。

也就是說,在網頁中找到一個“原子能”的匹配相當於找到九個“應用”的匹配。

利用 IDF,上述相關性計算的公式就由詞頻的簡單求和變成了加權求和,即 TF1IDF1 + TF2IDF2 +... + TFN*IDFN。

在上面的例子中,該網頁和“原子能的應用”的相關性為 0.0069,其中“原子能”貢獻了 0.0054,而“應用”只貢獻了0.0015。

這個比例和我們的直覺比較一致了。

詞庫

詞庫與idf

在輸入法中,有詞庫的概念,比如你從事的是IT行業,和同行溝通時,會用到很多IT行業的術語,詞庫的片語可用於切詞。

同一個詞語,在不同行業的詞庫中IDF應該是不一樣的,因為他們的集合不一樣。比如IT行業,總的文件數是100億。醫療行業總的文件數是10億。

“生物”這個詞在這兩個行業中的IDF會一樣嗎?

計算IDF

就像人口普查一樣,IDF的計算也可以做取樣,當然也可以全網計算,特別是搜尋引擎,每天都在爬各種網站的內容,搜尋引擎的內容應該算是最大的。

從搜尋引擎計算出來的IDF相對會比較準確,不過前面也說了,如果做垂直行業的關鍵字提取,需要建立行業自己的IDF,這樣提取的關鍵字準確度可以做到更高。

那麼如何計算IDF呢?實際上很簡單,以資料庫為例。

《聊一聊雙十一背後的技術 - 分詞和搜尋》

MADlib TF 統計介面

MADlib是Pivotal與伯克利大學合作的一個開源機器學習庫,可以用在Greenplum, PostgreSQL, HAWQ等資料庫中。

這裡面也有一個TF統計的介面

http://madlib.incubator.apache.org/docs/latest/group__grp__text__utilities.html

內容如下,呼叫term_frequency這個函式可以對儲存文字的表進行統計,得到每條記錄所有WORD的TF

Term frequency tf(t,d) is to the raw frequency of a word/term in a document, i.e. the number of times that word/term t occurs in document d.

For this function, 'word' and 'term' are used interchangeably.

Note: the term frequency is not normalized by the document length.

    term_frequency(input_table,    
                   doc_id_col,    
                   word_col,    
                   output_table,    
                   compute_vocab)    

引數解釋如下

Arguments:    

input_table    
  TEXT.     
  The name of the table storing the documents.     
  Each row is in the form <doc_id, word_vector> where doc_id is an id, unique to each document, and word_vector is a text array containing the words in the document.     
  The word_vector should contain multiple entries of a word if the document contains multiple occurrence of that word.    

id_col    
  TEXT.     
  The name of the column containing the document id.    

word_col    
  TEXT.     
  The name of the column containing the vector of words/terms in the document.     
  This column should of type that can be cast to TEXT[], 例如tsvector.    

output_table    
  TEXT.     
  The name of the table to store the term frequency output.     
  The output table contains the following columns:    
    id_col:     
      This the document id column (same as the one provided as input).    
    word:     
      A word/term present in a document. This is either the original word present in word_col or an id representing the word (depending on the value of compute_vocab below).    
    count:     
      The number of times this word is found in the document.    
    compute_vocab:    
      BOOLEAN. (Optional, Default=FALSE) Flag to indicate if a vocabulary is to be created.     
      If TRUE, an additional output table is created containing the vocabulary of all words, with an id assigned to each word.     
      The table is called output_table_vocabulary (suffix added to the output_table name) and contains the following columns:    
    wordid:     
      An id assignment for each word    
    word:     
      The word/term    

PostgreSQL 全文統計

PostgreSQL支援全文檢索型別(tsvector),使用ts_stat函式可以對全表統計,word出現的總次數,word出現的文字數。

可以用於提取全表的關鍵詞,也可以用於檢查分詞的有效性,如果發現文字中有大量的stop-word出現,說明分詞效果不好。

https://www.postgresql.org/docs/9.6/static/textsearch-features.html#TEXTSEARCH-STATISTICS

ts_stat函式用法如下

The function ts_stat is useful for checking your configuration and for finding stop-word candidates.    

ts_stat(sqlquery text, [ weights text, ]    
        OUT word text, OUT ndoc integer,    
        OUT nentry integer) returns setof record    

sqlquery is a text value containing an SQL query which must return a single tsvector column.     

ts_stat executes the query and returns statistics about each distinct lexeme (word) contained in the tsvector data.     

The columns returned are    

  word text — the value of a lexeme    

  ndoc integer — number of documents (tsvectors) the word occurred in    

  nentry integer — total number of occurrences of the word    

  If weights is supplied, only occurrences having one of those weights are counted.    

For example, to find the ten most frequent words in a document collection:    

例子

SELECT * FROM ts_stat('SELECT vector FROM apod')    
ORDER BY nentry DESC, ndoc DESC, word    
LIMIT 10;    
The same, but counting only word occurrences with weight A or B:    

SELECT * FROM ts_stat('SELECT vector FROM apod', 'ab')    
ORDER BY nentry DESC, ndoc DESC, word    
LIMIT 10;    

例子

postgres=# SELECT * from (values (tsvector 'a b c'),(to_tsvector( 'b c d b'))) t(word);    
        word             
---------------------    
 'a' 'b' 'c'    
 'b':1,4 'c':2 'd':3    
(2 rows)    

postgres=# SELECT * FROM ts_stat($$SELECT * from (values (tsvector 'a b c'),(to_tsvector( 'b c d b'))) t(word)$$);    
 word | ndoc | nentry     
------+------+--------    
 d    |    1 |      1    
 c    |    2 |      2    
 b    |    2 |      3    
 a    |    1 |      1    
(4 rows)    

在資料庫中如何訓練生成IDF

計算所有詞(包括stop word)的idf

測試表,每條記錄包含一個PK,同時包含一個文字

create table doc(id int primary key, info text);    

postgres=# insert into doc values (1,'hi i am digoal');    
INSERT 0 1    

postgres=# insert into doc values (2,'hi i am abc');    
INSERT 0 1    

使用對應的分詞(ts_config)配置,對每個文字進行分詞,並計算出word在整表的idf,記錄數越多,IDF越準確(類似文字訓練)

我們還需要用到ts_debug這個函式,它會得到這個詞的token type。

postgres=# select * from  ts_debug('a b c hello i am digoal');    
   alias   |   description   | token  |  dictionaries  |  dictionary  | lexemes      
-----------+-----------------+--------+----------------+--------------+----------    
 asciiword | Word, all ASCII | a      | {english_stem} | english_stem | {}    
 blank     | Space symbols   |        | {}             |              |     
 asciiword | Word, all ASCII | b      | {english_stem} | english_stem | {b}    
 blank     | Space symbols   |        | {}             |              |     
 asciiword | Word, all ASCII | c      | {english_stem} | english_stem | {c}    
 blank     | Space symbols   |        | {}             |              |     
 asciiword | Word, all ASCII | hello  | {english_stem} | english_stem | {hello}    
 blank     | Space symbols   |        | {}             |              |     
 asciiword | Word, all ASCII | i      | {english_stem} | english_stem | {}    
 blank     | Space symbols   |        | {}             |              |     
 asciiword | Word, all ASCII | am     | {english_stem} | english_stem | {}    
 blank     | Space symbols   |        | {}             |              |     
 asciiword | Word, all ASCII | digoal | {english_stem} | english_stem | {digoal}    
(13 rows)    

https://www.postgresql.org/docs/9.6/static/textsearch-parsers.html

預設的parser包括如下token type

Alias Description Example
asciiword Word, all ASCII letters elephant
word Word, all letters ma?ana
numword Word, letters and digits beta1
asciihword Hyphenated word, all ASCII up-to-date
hword Hyphenated word, all letters lógico-matemática
numhword Hyphenated word, letters and digits postgresql-beta1
hword_asciipart Hyphenated word part, all ASCII postgresql in the context postgresql-beta1
hword_part Hyphenated word part, all letters lógico or matemática in the context lógico-matemática
hword_numpart Hyphenated word part, letters and digits beta1 in the context postgresql-beta1
email Email address foo@example.com
protocol Protocol head http://
url URL example.com/stuff/index.html
host Host example.com
url_path URL path /stuff/index.html, in the context of a URL
file File or path name /usr/local/foo.txt, if not within a URL
sfloat Scientific notation -1.234e56
float Decimal notation -1.234
int Signed integer -1234
uint Unsigned integer 1234
version Version number 8.3.0
tag XML tag
entity XML entity &
blank Space symbols (any whitespace or punctuation not otherwise recognized)

統計IDF

with t1 as (    
  select count(*) as cnt from doc    
),    
t2 as (    
  select id, alias, token from     
    (    
      select id,(ts_debug(info)).* from doc    
    ) t    
  group by id, alias, token    
)    
select t2.token, t2.alias, log(t1.cnt/count(t2.*)) as idf from t1,t2 group by t2.token,t2.alias,t1.cnt;    


 token  |   alias   |        idf            
--------+-----------+-------------------    
        | blank     |                 0    
 hi     | asciiword |                 0    
 abc    | asciiword | 0.301029995663981    
 am     | asciiword |                 0    
 i      | asciiword |                 0    
 digoal | asciiword | 0.301029995663981    
(6 rows)    

使用PostgreSQL提取關鍵詞

如何提取每篇文件的關鍵詞?

1. 計算tf

計算每條記錄(假設每篇文字一條記錄)有多少詞

set default_text_search_config='pg_catalog.english';  

select id, length(to_tsvector(info)) as cnt from doc;  

計算每篇文件,每個詞出現了多少次

select id, (ts_stat('select to_tsvector(info) from doc where id='||id)).* from doc;  

還有一種方法計算tf

https://www.postgresql.org/docs/9.6/static/textsearch-controls.html#TEXTSEARCH-RANKING

ts_rank([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4
  Ranks vectors based on the frequency of their matching lexemes.

normalization
0 (the default) ignores the document length

1 divides the rank by 1 + the logarithm of the document length

2 divides the rank by the document length

4 divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd)

8 divides the rank by the number of unique words in document

16 divides the rank by 1 + the logarithm of the number of unique words in document

32 divides the rank by itself + 1

原始碼

src/backend/utils/adt/tsrank.c

Datum
ts_rank_ttf(PG_FUNCTION_ARGS)
{
        TSVector        txt = PG_GETARG_TSVECTOR(0);
        TSQuery         query = PG_GETARG_TSQUERY(1);
        int                     method = PG_GETARG_INT32(2);
        float           res;

        res = calc_rank(getWeights(NULL), txt, query, method);

        PG_FREE_IF_COPY(txt, 0);
        PG_FREE_IF_COPY(query, 1);
        PG_RETURN_FLOAT4(res);
}

Datum
ts_rank_tt(PG_FUNCTION_ARGS)
{
        TSVector        txt = PG_GETARG_TSVECTOR(0);
        TSQuery         query = PG_GETARG_TSQUERY(1);
        float           res;

        res = calc_rank(getWeights(NULL), txt, query, DEF_NORM_METHOD);

        PG_FREE_IF_COPY(txt, 0);
        PG_FREE_IF_COPY(query, 1);
        PG_RETURN_FLOAT4(res);
}


static float4
calc_rank_cd(const float4 *arrdata, TSVector txt, TSQuery query, int method)
{
        DocRepresentation *doc;
        int                     len,
                                i,
                                doclen = 0;
        CoverExt        ext;
        double          Wdoc = 0.0;
        double          invws[lengthof(weights)];
        double          SumDist = 0.0,
                                PrevExtPos = 0.0,
                                CurExtPos = 0.0;
        int                     NExtent = 0;
        QueryRepresentation qr;


        for (i = 0; i < lengthof(weights); i++)
        {
                invws[i] = ((double) ((arrdata[i] >= 0) ? arrdata[i] : weights[i]));
                if (invws[i] > 1.0)
                        ereport(ERROR,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("weight out of range")));
                invws[i] = 1.0 / invws[i];
        }

        qr.query = query;
        qr.operandData = (QueryRepresentationOperand *)
                palloc0(sizeof(QueryRepresentationOperand) * query->size);

        doc = get_docrep(txt, &qr, &doclen);
        if (!doc)
        {
                pfree(qr.operandData);
                return 0.0;
        }

        MemSet(&ext, 0, sizeof(CoverExt));
        while (Cover(doc, doclen, &qr, &ext))
        {
                double          Cpos = 0.0;
                double          InvSum = 0.0;
                int                     nNoise;
                DocRepresentation *ptr = ext.begin;

                while (ptr <= ext.end)
                {
                        InvSum += invws[WEP_GETWEIGHT(ptr->pos)];
                        ptr++;
                }

                Cpos = ((double) (ext.end - ext.begin + 1)) / InvSum;

                /*
                 * if doc are big enough then ext.q may be equal to ext.p due to limit
                 * of posional information. In this case we approximate number of
                 * noise word as half cover's length
                 */
                nNoise = (ext.q - ext.p) - (ext.end - ext.begin);
                if (nNoise < 0)
                        nNoise = (ext.end - ext.begin) / 2;
                Wdoc += Cpos / ((double) (1 + nNoise));

                CurExtPos = ((double) (ext.q + ext.p)) / 2.0;
                if (NExtent > 0 && CurExtPos > PrevExtPos               /* prevent devision by
                                                                                                                 * zero in a case of
                                multiple lexize */ )
                        SumDist += 1.0 / (CurExtPos - PrevExtPos);

                PrevExtPos = CurExtPos;
                NExtent++;
        }

        if ((method & RANK_NORM_LOGLENGTH) && txt->size > 0)
                Wdoc /= log((double) (cnt_length(txt) + 1));

        if (method & RANK_NORM_LENGTH)
        {
                len = cnt_length(txt);
                if (len > 0)
                        Wdoc /= (double) len;
        }

        if ((method & RANK_NORM_EXTDIST) && NExtent > 0 && SumDist > 0)
                Wdoc /= ((double) NExtent) / SumDist;

        if ((method & RANK_NORM_UNIQ) && txt->size > 0)
                Wdoc /= (double) (txt->size);

        if ((method & RANK_NORM_LOGUNIQ) && txt->size > 0)
                Wdoc /= log((double) (txt->size + 1)) / log(2.0);

        if (method & RANK_NORM_RDIVRPLUS1)
                Wdoc /= (Wdoc + 1);

        pfree(doc);

        pfree(qr.operandData);

        return (float4) Wdoc;
}

2. 計算idf

with t1 as (    
  select count(*) as cnt from doc    
),    
t2 as (    
  select id, alias, token from     
    (    
      select id,(ts_debug(info)).* from doc    
    ) t    
  group by id, alias, token    
)    
select t2.token, t2.alias, log(t1.cnt/count(t2.*)) as idf from t1,t2 group by t2.token,t2.alias,t1.cnt;   

3. 計算每個詞的tf-idf

tf * idf  

4. 將以上邏輯寫成函式即可提取tf*idf值的TOPN詞即文字的關鍵詞

參考

http://baike.baidu.com/view/1228847.htm

https://en.wikipedia.org/wiki/Tf%E2%80%93idf

《如何加快PostgreSQL結巴分詞載入速度》

《聊一聊雙十一背後的技術 - 分詞和搜尋》

相關文章