#はじめに
いろんな方が実装されているものの寄せ集めです。
(敬意を込めて参照先を記さないと。追々やっていきます。)
やろうとしていることは、
・ドキュメントをBertでベクトル化
・Elasticsearchでコサイン類似検索
という感じで、辞書を作ることなく、だいたい同じような意味合いの文書をヒットさせるという環境の構築をします。
#環境を構築してみる
root権限とかない一般ユーザ環境のUbuntu 16.04.3でお試しです。
Python3.Xは入っているものとしてください。
素人なのでいかんせん違うところがあるかもです。
文体に力がないのは外が暗くて眠いからです。
改めて見るといかんせん文体暗すぎですね。(Mar.2.20)
###Pytorchをインストール
Bertを使うのに今回はPytorchを使うことにしました。
そう決めたのは確かchainerが開発停止を発表したときだったと思います。
個人的にはあまり良くわかっていなかったのですが、めっちゃ詳しい人が、これからはPytorchがアツい的なことを言っていたので深く考えずにそうしました。
pip install torch==1.1.0 torchvision==0.3.0 --user
Pytorchはcudaによるので、環境を調べて最適なインストールをしたほうがいいです。詳しくはここをみてください。
権限があるなら別に--userはいらないです。
(root権限がないこと前提なのでつけないとですね)
###sumyのインストール
文書要約ライブラリ。特に何もなくそのまんまのインストールです。
pip install sumy --user
###mojimojiのインストール
全角半角コンバータ。同じくそのまんまのインストールです。
pip install mojimoji --user
###pyyamlのインストール。
yamlライブラリ
pip install pyyaml --user
###Juman++のインストール
京都大学の黒橋・河原研究室で開発されたもの。
こういう研究のおかげで僕らは楽ができるんですね。本当にありがとうございます。
http://nlp.ist.i.kyoto-u.ac.jp/index.php?JUMAN++
#ユーザ域に格納先を作成しておく
mkdir ~/usr/local/bin
で、ここからがJuman++インストール本番
#ソースコード取得
wget https://github.com/ku-nlp/jumanpp/releases/download/v2.0.0-rc3/jumanpp-2.0.0-rc3.tar.xz
# 解凍してディレクトリに入る
tar xvf jumanpp-2.0.0-rc3.tar.xz
cd jumanpp-2.0.0-rc3
# ビルド用ディレクトリを作成・移動
mkdir bld
cd bld
# ビルド
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$HOME/usr/local
make
make install
#作業域削除
rm -rd jumanpp-2.0.0-rc3
###Bertの準備
ちなみにここに出てくる学習済みセットも黒橋・河原研究室の成果物です。
頭が上がりません・・・。
http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT%E6%97%A5%E6%9C%AC%E8%AA%9EPretrained%E3%83%A2%E3%83%87%E3%83%AB
#基本パッケージインストール
pip install pytorch-pretrained-bert --user
pip install pyknp --user
#学習済みファイルのダウンロード
wget http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/JapaneseBertPretrainedModel/Japanese_L-12_H-768_A-12_E-30_BPE.zip
unzip Japanese_L-12_H-768_A-12_E-30_BPE.zip
rm Japanese_L-12_H-768_A-12_E-30_BPE.zip
###Elasticsearchのインストール
#取得
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.5.0-linux-x86_64.tar.gz
#解凍
tar xvf elasticsearch-7.5.0-linux-x86_64.tar.gz
#パスを変え、日本語プラグインをインストール
cd elasticsearch-7.5.0
bin/elasticsearch-plugin install analysis-kuromoji
#kibanaもインストールしておこう
cd
wget https://artifacts.elastic.co/downloads/kibana/kibana-7.5.0-linux-x86_64.tar.gz
#解凍
tar xvf kibana-7.5.0-linux-x86_64.tar.gz
#格納場所変更
mv ~/elasticsearch-7.5.0 ~/usr/local/elasticsearch
mv ~/kibana-7.5.0-linux-x86_64 ~/usr/local/kibana
#Pythonで制御できるようにする
pip install elasticsearch --user
###環境変数とか調整
#環境変数の設定
vi .profile
.profileに以下を書き加え保存
export ELASTIC_HOME="$HOME/usr/local/elasticsearch/bin"
export KIBANA_HOME="$HOME/usr/local/kibana/bin"
export PATH="$ELASTIC_HOME:$KIBANA_HOME:$HOME/usr/local/bin:$PATH"
再ログイン又は以下のコマンドで設定反映
#即時反映
source .profile
#コマンドで確認
echo $PATH
###動作確認
#####Juman++の動き確認
#バージョン表示
jumanpp -v
#動作確認
echo "適当な文字列を判定させてみる" | jumanpp
適当な文字列を判定させてみる、が形態素分析されたら成功
#####ElasticsearchとKibanaの起動
elasticsearch -d
kibana
一般ユーザとしての起動なのでサービス化はしてないです。
これで普通に通れば環境変数がちゃんと設定されてると思います。
kibanaのポートは5601がデフォルトなので、Webページが表示されればOKかと。
ちなみに、elasticsearchがインストールされてないと、kibanaが起動しないので、
kibanaの起動で両方いっぺんに確認できる、とおもう。
#Pythonで動かしてみる
文章を20センテンスくらいに圧縮して、半角全角コンバートして、Bertベクトル化する、というもの。クラス化してもいいのかもしれない。
JavaとかC#だと積極的にそうするんだけど、Pythonだとどうも気乗りしない。
selfてどうよ。
BERT_BASE_DIR = "/[学習済みセットのあるところ]/Japanese_L-12_H-768_A-12_E-30_BPE/"
VOCAB_FILE = "vocab.txt"
import mojimoji
import re
from pyknp import Juman
import numpy as np
import torch
from pytorch_pretrained_bert import BertTokenizer, BertModel
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lex_rank import LexRankSummarizer
juman = Juman()
bert_tokenizer = BertTokenizer(BERT_BASE_DIR + VOCAB_FILE, do_lower_case=False, do_basic_tokenize=False)
use_cuda = False
model = BertModel.from_pretrained(BERT_BASE_DIR)
#------------------------------------------------------------------
# 一文から、名詞、形容詞、副詞、動詞のみで、分かち書きの一文を返す
#------------------------------------------------------------------
def __get_wakachi(sentence):
target = mojimoji.han_to_zen(sentence)
temp = juman.analysis(target)
result = [mrph.genkei for mrph in temp.mrph_list() if mrph.hinsi in ('名詞', '形容詞', '副詞', '動詞')]
return ' '.join(result) + ' 。'
#------------------------------------------------------------------
# 分かち書きの文章リストから、BERTトークンを作成する
#------------------------------------------------------------------
def __get_bert_tokens(wakachi_list):
bert_tokens = [bert_tokenizer.tokenize(wakachi)[:126] + ['[SEP]'] for wakachi in wakachi_list]
return ['[CLS]'] + sum(bert_tokens, [])
#------------------------------------------------------------------
# BERT_TOKENSから文書全体のベクトルを生成する
#------------------------------------------------------------------
def __get_sentence_embedding(bert_tokens, pooling_layer=-2, pooling_strategy='REDUCE_MEAN'):
ids = bert_tokenizer.convert_tokens_to_ids(bert_tokens)
tokens_tensor = torch.tensor(ids).reshape(1, -1)
if use_cuda:
tokens_tensor = tokens_tensor.to('cuda')
model.to('cuda')
torch.cuda.set_device(4)
model.eval()
with torch.no_grad():
all_encoder_layers, _ = model(tokens_tensor)
embedding = all_encoder_layers[pooling_layer].cpu().numpy()[0]
if pooling_strategy == "REDUCE_MEAN":
return np.mean(embedding, axis=0)
elif pooling_strategy == "REDUCE_MAX":
return np.max(embedding, axis=0)
elif pooling_strategy == "REDUCE_MEAN_MAX":
return np.r_[np.max(embedding, axis=0), np.mean(embedding, axis=0)]
elif pooling_strategy == "CLS_TOKEN":
return embedding[0]
else:
raise ValueError("specify valid pooling_strategy: {REDUCE_MEAN, REDUCE_MAX, REDUCE_MEAN_MAX, CLS_TOKEN}")
#------------------------------------------------------------------
# 要約をする
#------------------------------------------------------------------
def __get_summary(wakachi_list, SENTENCES_COUNT):
#summarizer設定
parser = PlaintextParser.from_string(''.join(wakachi_list), Tokenizer('japanese'))
summarizer = LexRankSummarizer()
summarizer.stop_words = [' ']
summary = summarizer(document=parser.document, sentences_count=SENTENCES_COUNT)
#要約選出された文章の分かち書きリスト作成
result = [wakachi_list[wakachi_list.index(sentence.__str__())].strip() for sentence in summary]
return result
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#ドキュメントからベクトルを取得する
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def get_vector(doc):
works = re.split('\n+',doc.replace('。', '。\n'))
wakachi_list = [__get_wakachi(i) for i in works]
if len(wakachi_list) > 20 :
wakachi_list = __get_summary(wakachi_list, 20)
print(len(wakachi_list))
bert_tokens = __get_bert_tokens(wakachi_list)
result = __get_sentence_embedding(bert_tokens)
return result
#CSVからElasticsearchにデータを流し込むとき
あらかじめ、yamlにElasticsearchへの定義を書いておきます。
例えば、data.yamlとして以下のように記します。
settings:
index:
analysis:
analyzer:
my_analyzer:
type: custom
tokenizer: kuromoji_tokenizer
filter:
- kuromoji_baseform
mappings:
properties:
article_date:
type: keyword
title:
type: text
index: true
analyzer: my_analyzer
article:
type: text
index: true
analyzer: my_analyzer
source:
type: keyword
article_vector:
type: dense_vector
dims: 768
article_vectorというやつがBertのベクトルを入れるところですね。
用意するCSVは、新聞記事を想定し、
掲載日時,タイトル,記事,提供元
としました。ファイル名はarticle.txtです。
掲載日時をタイムスタンプ型にしていないのは、日付変換がめんどくさそうだったからです。
そしたら、以下のコードでCSVを読み込み、Elasticsearchに登録します。
import csv
import os
import sys
from elasticsearch import Elasticsearch, helpers
import yaml
def create_index(index='articles'):
es = Elasticsearch()
#全部消して再定義する場合
#print(es.indices.delete(index=index, ignore=[404]))
setting = yaml.load(open('./data.yaml'), Loader=yaml.SafeLoader)
properties = setting['mappings']['properties']
print(setting)
#新規に作成する場合は以下をコメントアウト。データ追加の場合はコメント
#print(es.indices.create(index=index, body=setting))
#print(es.indices.flush())
def generate_data():
with open('./article.txt', 'r') as f:
reader = csv.reader(f)
attrs = next(reader)
for lid, row in enumerate(reader):
data = {
'_op_type': 'index',
'_index': index,
}
for j, value in enumerate(row):
if attrs[j] in properties:
data[attrs[j]] = value
if attrs[j] == 'article':
data['article_vector'] = get_vector(value).tolist()
yield data
print(helpers.bulk(es, generate_data()))
create_index()
あんまり大量のデータを流し込もうとするとエラーが出る。
bulkの使い方がなっていないのかもしれない。調査せねば。
#検索の方法(Pythonから)
こんな感じで検索します。
反応早くてびっくりです。
from elasticsearch import Elasticsearch
es = Elasticsearch()
INDEX = "articles"
#ベクトルのみでの検索。もっと長文でも構わないです。
query_vector = get_vector('''「仁義なき戦い」''').tolist()
query_script = {
"script_score": {
'query': { 'match_all': {}},
'script': {
"source": "cosineSimilarity(params.query_vector, doc['article_vector'])",
"params": {"query_vector": query_vector}
}
}
}
#文字列自体の部分一致と組み合わせて検索する場合
'''
query_script = {
"script_score": {
"query": {"wildcard": {
"article": {
"value": "*深作欣二*"
}
}},
'script': {
"source": "cosineSimilarity(params.query_vector, doc['article_vector'])",
"params": {"query_vector": query_vector}
}
}
}
'''
response = es.search(
index=INDEX,
body={
"size": 10,
"query": query_script,
"_source": {"includes": ["title", "article"]}
}
)
print(response)
こんなところです。まだまだ勉強ですね。
もっと勝手な環境で勝手にあれやこれやしたいんだよ、ということもあるのかな、というわけでCentOS8上に管理者権限使いまくりで環境設定する方法も書いてみました。