#はじめに
Qiitaの投稿に慣れていきます。。
Dockerによる環境構築やAWSによる、「PCローカル環境依存」への脱却をしようとしているこの頃。
いろんな学習をしているうちに、ローカルにタコ足配線みたく崇高な設定だらけになって、
もっとシンプルにそして、PCの買い替えの時に困らないようにしたいよね〜という人も少なくないはず
Docker構築
↓
コンテナ上で作業
↓
スクレイピングとかでデータ収集
↓
せっかくなので、多クラス分類
をしてみました。
全てgit上に上げているのと、今後gensimとか使って各評価の文章要約でもしようかと思っています。
また、その時に書きます。
https://github.com/Yu0130fri/Scraping-NLP
この投稿だけでDocker完璧!とか、多クラス分類できるようになる!
みたいな崇高な記事ではないことも先に書いておきます(^^;)
#Dockerfileによる環境構築
自分の勝手な主題としてdockerfileやdockerhubからのpullで(簡単に誰でも)環境を構築できるように練習したいのが主題。
今回スクレイピングで楽天トラベルのサイトからレビューを持ってくるため、形態素解析にMeCabやneologdなどを個々人でセットアップするのだるいよね〜〜
という背景があり、Dockerなら共通でいけるやん〜みたいな感じでdockerの勉強がてら作りました。
gitコード上のDockerフォルダにDockerfileは作っていますが、普通に
でいけます。
dockerのimageは重くなりがち(たしか今回使うイメージも7GB。。。)なので、
ローカルのストレージが足りないときは AWS上で実行するのがおすすめなんですが、
AWS上でdockerを使えるようにしないといけなかったりもあるので、任せますw
とはいえ、常にimageを残す必要もないので、実行は
みたいに--rmコマンドで終了したら削除するようにして、その後rmiコマンドですぐに消すとかが個人的推奨。
一応、Dockerfileの中身を入れときます。
FROM ubuntu:latest
RUN apt update && apt install -y \
curl \
file \
git \
libmecab-dev \
make \
mecab \
mecab-ipadic-utf8 \
sudo \
wget \
vim \
xz-utils
# install anaconda3 you can change the version if you want to
WORKDIR /opt
RUN wget https://repo.anaconda.com/archive/Anaconda3-2021.05-Linux-x86_64.sh && \
sh Anaconda3-2021.05-Linux-x86_64.sh -b -p /opt/anaconda3 && \
rm -f Anaconda3-2021.05-Linux-x86_64.sh
ENV PATH=/opt/anaconda3/bin:$PATH
RUN pip install --upgrade pip && \
pip install mecab-python3 && \
pip install mojimoji && \
pip install unidic-lite
WORKDIR /
# get the ipadic-neologd
RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git && \
echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -a
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"]
anaconda のインストールから、MeCab・neologdのセットアップまでしていて、すぐに
import MeCab
を使えるようにしました。
#スクレイピング
特に複雑なスクレイピングコードでもなく、オーソドックスにrequests, bs4で取得したい項目を拾ってきています。
スクレイピングについてガツガツ解説するのは、のちのち。
def make_reputation_csv(AREA_URL='https://travel.rakuten.co.jp/yado/aichi/A.html'):
try:
soup_area = modules.get_soup(AREA_URL)
hotels = soup_area.select('section > div.htlHead> div.info > span')
for hotel in hotels:
hotel_ID = hotel['id'][-6:]
hotel_review_url = f'https://travel.rakuten.co.jp/HOTEL/{hotel_ID}/review.html'
# get data souce
soup_hotel = modules.get_soup(hotel_review_url)
# hotel name
hotel_name = soup_hotel.select("#RthNameArea > h2")[0].text
_reputations, _comments, _purposes, _companions, _dates = modules.make_object_lists(soup_hotel)
# make df and to csv
data = pd.DataFrame(
{'HotelName': hotel_name,
'Date': _dates,
'Reputations': _reputations,
'Comments': _comments,
'Purposes': _purposes,
'companions': _companions}
).to_csv(f'./sample_csv/{hotel_ID}_reputation.csv', encoding='utf-8', index=False)
time.sleep(2)
except:
print('実行は終了されました')
このコードのsleep時間が短いかもなので、多用したらすぐにサイトから弾き出されるので、一回だけって感じです。
#多クラス分類
今回は精度向上の目標が本題ではないので、SVM(一応RandomizeSearchCVは使った), MultinomialNBでどのくらいの精度が出たのかくらいをみました。
全て RakutenTravel_sc_predict.ipynbで作業しているので、いろいろ追いやすいかなと思いますが、一応ここでも少し書いときます。
データ読み込み〜前処理
一応今回欠損値はcsvにはないので、補完作業はいらないです。
また、今回は分析に使わなかったですが、object-> datetimeに変換と
本題の形態素への変換だけ行いました。
# データ読み込みを一括で
csv_lists = glob('./sample_csv/*')
data_dict = {}
preffixes = 'data'
for i, csv in enumerate(csv_lists):
data_dict[preffixes+str(i+1)] = pd.read_csv(csv)
data = pd.concat([data for data in data_dict.values()]).reset_index(drop=True)
多少癖のある読み込み方かもですが、いちいちread_csvするのも今回はめんどくさいので、
dictに格納し、一気に読み込ませました。
これによりいちいちcsvの名前を書き込む手間も、命名も簡単に行えるので、最近勉強したなかでかなり有意義なコードでしたので、ここでも共有しておきます
preffixを先に定義することで{data1: (Dataframe), data2: (DataFrame), ...}のように簡単に連番を振ることができます。
データ読み込めば、次は日付変換・形態素解析をしておきます。
data['Date'] = pd.to_datetime(data['Date'], format='%Y年%m月')
data['Comments'] = [word.strip() for word in data['Comments']] # 不要な\n, \tを削除
data['Comment_words'] = data['Comments'].apply(lambda x: text_to_word(x))
このtext_to_wordは文書を形態素解析して、stopwordの除去、全ての形態素を原型に置換する関数です。
割と個人的に便利と思っている関数
tagger = MeCab.Tagger("-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")
tagger.parse("")
def text_to_word(text, stopword_pass="./stopwords/Japanese.txt"):
"""
Tokenize texts with MeCab neologd
Parameters
----------
text: str
text to tokenize
stopword_pass: str
path of stopwords
Returns
-------
basic_words_list: str
tokenized and basic text
"""
#stopwordのリスト作成
stopword_list =[]
with open(stopword_pass, "r") as file:
lines = file.readlines()
stopword_list = [stopword for stopword in lines if stopword.strip()]
#mojimojiで全角数字、英字を半角に統一。大文字もすべて小文字に統一する
text = mojimoji.zen_to_han(text, kana=False).lower()
#解析
parse_text = tagger.parse(text)
#ここから解析したものの原型などを取り出して抽出していく
basic_words_list = []
#各単語の解析結果ごとにsplitしていく
split_parse_text = parse_text.split("\n")
for parse_word in split_parse_text:
#\tで区切り、表層と品詞の情報に分ける
split_parse_word = parse_word.split("\t")
surface_word = split_parse_word[0]
#最終行はEOSのため、終了させる
if surface_word =="EOS":
break
else:
#品詞が動詞、形容詞の場合、原形を格納
morph_info = split_parse_word[1]
morphs = morph_info.split(",")
#品詞情報
morph = morphs[0]
#原型
#原型は品詞情報の後ろから3番目
basic = morphs[-3]
if morph == "記号":
continue
elif morph in("動詞", "形容詞") and basic not in stopword_list:
basic_words_list.append(basic)
elif morph =='名詞' and basic not in stopword_list:
basic_words_list.append(basic)
#最終的にまとめたものを半角スペースでjoinし、リストで返す
basic_words_list = " ".join(basic_words_list)
return basic_words_list
なっっがいですが、割とシンプルなコードかなと思います。
逐次的な説明は端折りますが、ここでMeCabとか使えるのはDockerのおかげw
#多クラス分類
前処理があらかた終わったので、分析へ
今回、Tfidfを使うので、sprase marixが出力として出てくるので、実際扱い方の練習にもなりました。
data_copy = data.copy()
X = data_copy[['Comment_words', 'Purposes', 'companions']]
X = pd.get_dummies(X, columns=['Purposes', 'companions'], drop_first=True)
y = data_copy['Reputations']
tfidf = TfidfVectorizer(min_df=4, max_df=.7)
x = tfidf.fit_transform(X['Comment_words'])
X_sparse = X.drop('Comment_words', axis=1).astype(pd.SparseDtype("int", np.nan))
X = hstack((x, X_sparse))
TfidVecがsparce matを返すため、get_dummiesで作られたデータもsprace matに変換する必要が(多分)あります
このへん、自分も怪しいのでもっといい方法があると思うので、調べときます。
あと、sprase matへの変換方法もastypeしかないのか?っていうのもあり、非常に微妙なコードですw
ま、気をとりなおして分析へ
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
nb = MultinomialNB()
nb.fit(X_train, y_train)
y_pred = nb.predict(X_test)
print(accuracy_score(y_true=y_test, y_pred=y_pred))
print(f1_score(y_true=y_test, y_pred=y_pred, average='macro'))
0.5714285714285714
0.1865531914893617
まずはTfidfと相性がいいMultinomialNBから。
KaggleのNLPのデータセットでこれ使っている人見て勉強しました。
多クラスかつ離散のときつかえます
とはいえ、rawデータはやはり精度も高くないすね〜〜
Tfidfのハイパーパラメータも少しいじりましたけど、そんなでした。
ラスト。
SVMでやってみます。
ま、本命ですかね?w
param_dst = {'C': np.arange(0.01, 10, 0.3), 'kernel': ['rbf', 'poly'], 'gamma': [0.01, 0.1]}
RS = RandomizedSearchCV(estimator=SVC(), param_distributions=param_dst, n_iter=50, cv=10, random_state=123)
RS.fit(X_train, y_train)
y_pred2 = RS.predict(X_test)
print('f1', f1_score(y_true=y_test, y_pred=y_pred, average='macro'))
print('acc', accuracy_score(y_true=y_test, y_pred=y_pred2))
f1 0.17315837339223075
acc 0.6134453781512605
お!ちょいあがりましたw
けど、頑張っても61%なので、むむむ〜〜って感じで、実践的なモデル構築とまではいってないですな。
#まとめ
今回はDockerによる環境構築をしてみたい!というところから始まり、データをスクレイピングで集めて多クラス分類までしてみました。
実際にはgensimのword2vecとかで文章要約とかはコード作ろうかな〜と思っているんで、その時はまた書きます。
あと、noteにいろんなコラム書いてたりするので、それをQiitaに移行しようかしら?とか思ってたりもします。
もし何かしらの役に立てれば幸いです!