エビフライの唐揚げ

今までのことや、技術的なこと、その他を書いていこうと思います。

Twitter上でAI女子高生が話すアプリを作成した件について

女子高生AIからのリプライが返ってくるアプリを作成した(過去)

【概要】

リプライを送ると女子高生AIからのリプライが返ってくるアプリ

【言語】

C#

【サーバー】

自宅PC

【DB】

SQLite 

 

 

f:id:littlemore:20200526135714g:plain



設計書

f:id:littlemore:20200526135949p:plain

 

ソリューションはこんな感じ

f:id:littlemore:20200526140116p:plain

 

Twitter上で遊べる非同期オンラインゲームと同じように下記ライブラリを使用している

CoreTweet:TwitterAPIを簡単に使用できる

Magick.NET:BitmapリストからGifアニメーションを作れる

 

感情分析機能を搭載していて、文章によって表情が変わったりします

感情分析には、ネガポジ判定を採用しました。

データセットどこから持ってきたのか忘れた・・・

転がってたのをDBに突っ込んだと思います

中身はこんな感じです

f:id:littlemore:20200526141837p:plain

ネガポジ判定なので、C#のソースは単純ですね

/// <summary>
/// 文章のネガポジ極性を求めます。
/// 文章の最後の単語の方が、極性の重きを置きます。
/// </summary>
/// <returns></returns>
public double PhrasePolarity(string phrase ,string DBpath)
{
	double magnification = 0;

	var wordList = new TinySegmenter().Segment(phrase);

	int judgeCount = 0;
	List<double> polarities = new List<double>();
	double polaritySum = 0;

	foreach (string word in wordList)
	{
		var polarity = new FealingJudge().EmotionGet(DBpath, word);
		if (polarity.HasValue && polarity != 0)
		{

			polarities.Add((double)polarity);

			if ((double)polarity == 0) continue;
			judgeCount++;
		}
	}

	magnification = 2.0 / (polarities.Count + 1);

	int magniCount = 0;
	foreach (var polar in polarities)
	{
		magniCount++;
		polaritySum += (polar * (magnification * magniCount));

	}
	return polaritySum / judgeCount;
}

 

 

Twitterのリプライを収集して、DBに突っ込んだものを検索して返してるだけです

ReplyDBの中はこんな感じ(不適切な表現があったので隠しています)

f:id:littlemore:20200526142904p:plain

ReverseTweet行があるのは、DBでLike文使ってSelectするとき、前方一致でしかインデックスが使われないためです。

 

後ろから検索する形になるので(その方がより会話っぽくなる)

例えば、「テスト満点おめ!」とリプを送ると

1.リバースする「テスト満点おめ!」→「!めお点満トステ」

2.検索します「!めお点満トステ」

3.見つからない場合、1文字削り検索します「!めお点満トステ」→「!めお点満トス」

4.2と3を繰り返し「!めお」まで削れたところで「あざまる水産」がヒット

 

という処理内容になっています。


こちらも、Twitter上で遊べるゲームと同じ原因で今は停止しています。

マネタイズ出来るなら、譲ります。配当金ほしいです!

 

pythonを始めた理由

仕事でpythonを使っていて、C#と比べて色んなことが楽にできるライブラリが充実していて、いいなーと思ってpythonを使用し始めた。

pandasとかめちゃくちゃ便利。C#で200行とかゴリゴリ書いてたのが、3行くらいで同じ処理が出来るようになったり・・・。

今は仕事で使っていないが、今までほぼC#しかやっておらず、C#だけしか出来ないというのも不安だと言うことも理由の一つである。

 

ほんとうにpythonは色んな処理が簡単に書ける

また、インタプリタ型言語でビルドが必要ない

ただ、ビルドが必要ないからこそ、ビルドエラーがお傷、構文エラーが見つけづらい

 

でもそれも、VisualStudioCode等を使うことで構文エラーを検知したり、ライブラリ情報も見れる

 

ここまで来ると相当開発しやすい

f:id:littlemore:20200526100340p:plain

 

Word2Vecによる単語計算のアプリをHerokuにデプロイした話:Word2VecのModel作成編

【アプリ名】
Word2Vecによる単語計算
【概要】
Word2Vecによる単語計算を行えるWebアプリ

Word2Vecによる単語計算

【言語】

Python

【サーバー】

Heroku

 

 

 

単純にWord2Vecを試したいだけであれば、学習済みのデータが転がっているので、それをダウンロードして読み込めばすぐにできる

しかし、Herokuにデプロイするには容量の問題が発生する

有料プランでアレば問題ないのだが、無料プランでは500MBまでしかストレージが使用できず、転がっている学習済モデルだと500MBを超えてしまう

なので、モデルから作った

 

1.Wikipediaの文章データをダウンロードする

2.wikiextractorを使用して、ダウンロードしたダンプデータからTextファイルにする

下記を参考に2まで行ってください

自分はwindows環境なのですが、下記のサイトではLinux環境のようなので

Window環境の方はこのあとの手順はこちらに書きます

kzkohashi.hatenablog.com

python WikiExtractor.py jawiki-latest-pages-articles.xml.bz2

ここまで完了するとtextディレクトリ配下にたくさんのテキストファイルが出来ると思います

3.バラけたテキストファイルをまとめつつ、不要な文字列を削除

import glob
import re


files =  glob.glob("./text/*/wiki_*", recursive=True)

for file in files:
    with open("./wiki.txt", mode='a', encoding="utf_8") as wf:
        with open(file, encoding="utf_8") as f:
            text = f.read()
            text = re.sub('<doc id.*>', '',text)
            text = re.sub('</doc>', '',text)
            wf.write(text)

 

4.3で作成したテキストを下記を参考にMecab形態素解析を行う

qiita.com

 

下記のようなテキストファイルが出来ればOK(4GB弱あるので、メモ帳だと開けない・・・)

f:id:littlemore:20200526093622p:plain

 

5.gensimを使用してModel作成

from gensim.models import word2vec
import logging
print("First")

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
sentences = word2vec.Text8Corpus('./wakati.txt')

model = word2vec.Word2Vec(sentences, size=300, min_count=50, iter=10)
model.wv.save_word2vec_format("./wiki.model", binary=True)

min_count=50で50個以上ある単語でのみモデルを作成するような設定になっています。

これで160MBくらいのモデルが作成できます

 

pipでgensimをインストールするだとか、Mecabno使い方だとかすべてすっ飛ばしていますが、他のサイトに詳しく書かれているのでいいかなぁ、と。

需要がアレば記事があるので、その際はコメントください

Twitter上で出来るゲームを作成した件について

Twitter上で遊べるゲームを作成した(過去)

【概要】

リプライを送ると戦闘結果が返ってくる非同期オンラインRPG

f:id:littlemore:20200526005757g:plain

 

【言語】

C#

【サーバー】

自宅PC

【DB】

SQLite

 

大まかな内部処理

f:id:littlemore:20200526010307p:plain

 

SVNの履歴を見る限り、約1年前につくったもの。

なので、あまりコードは覚えてない・・・。

Twitterに挙げられるGifのサイズが3MB以下だったりしたので、2ターンで決着を付けさせたり、画像サイズを圧縮したり、見えないところで努力をした記憶がある。

 

このプログラムで使った主なライブラリは、

CoreTweet:TwitterAPIを簡単に使用できる

Magick.NET:BitmapリストからGifアニメーションを作れる

 

一応、これが戦闘計算用クラスにあったメソッド

ソースコードはあまり綺麗とは言えない・・・。

 

public static List<BattleData> BattleDataCreator(BattleData battleData, List<CommandSkill> actorSkills)
{
	List<BattleData> battleDataList = new List<BattleData>();
	battleDataList.Add(battleData);
	var characterData = battleData.CharacterData;
	characterData.ForEach(p => InitializeCharacterData(p));
	bool extinction = false;

	int trun = 0;
	while (trun < 2)
	{
		trun++;
		Logger.write("ターン" + trun.ToString(), LogType.Debug);
		//素早さが早い順に並び替え
		characterData = characterData.OrderByDescending(p => p.Parameters.SPD).ThenBy(a => Guid.NewGuid()).ToList();
		for (int k = 0; k < characterData.Count; k++)
		{
			characterData[k].Status.Order = k;
		}

		//全員行動済みになるまで
		for (int i = 0; i < characterData.Count; i++)
		{
			//行動準備初期化
			var battleDataTemp = DeepClone(battleDataList.Last()) as BattleData;
			battleDataTemp.CharacterData.ForEach(p => p.Status.Animetion = Animetion.Defalt);
			battleDataTemp.CharacterData.ForEach(p => p.Status.Damage = 0);
			battleDataTemp.CharacterData.ForEach(p => p.Status.PictureMessage = string.Empty);

			//HPが0の場合戦闘不能フラグ
			battleDataTemp.CharacterData.Where(p => p.Parameters.HP <= 0).ToList().ForEach(q => q.Status.Death = true);

			characterData = battleDataTemp.CharacterData;
			//スキルコマンドをキャラにセット
			SetActorSkill(ref characterData, actorSkills, trun);

			//行動準備順に並び替え
			characterData = characterData.OrderBy(p => p.Status.Order).ToList();
			Logger.write("処理," + i.ToString() + "," + characterData[i].Parameters.Name, LogType.Debug);
			//戦闘不能の場合スキップ
			if (characterData[i].Status.Death) continue;
			Logger.write("行動," + i.ToString() + "," + characterData[i].Parameters.Name, LogType.Debug);

			//行動準備					
			var skill = characterData[i].Status.Skills[new RandomEx().Next(0, characterData[i].Status.Skills.Count)];
			Logger.write("スキル," + i.ToString() + "," + characterData[i].Parameters.Name + skill.SkillName, LogType.Debug);
			if (characterData[i].Parameters.HP - skill.Cost < 1) skill = new Skill_Attack();
		
characterData[i].Status.Animetion = skill.UserAnime; battleDataTemp.CommandMessage = characterData[i].Parameters.Name + skill.SkillMessage + characterData[i].Status.ActorMessage; battleDataList.Add(battleDataTemp); //行動準備初期化 battleDataTemp = DeepClone(battleDataList.Last()) as BattleData; battleDataTemp.CharacterData.ForEach(p => p.Status.Animetion = Animetion.Defalt); var actionCharaData = battleDataTemp.CharacterData; //行動準備順に並び替え actionCharaData = actionCharaData.OrderBy(p => p.Status.Order).ToList(); //行動(全体攻撃対応したので、ターゲットが複数の場合がある) var targetDatas = TargetSeacher(actionCharaData, skill.Target, actionCharaData[i].Actor); if (targetDatas.Where(p => p != null).Count() == 0) { continue; } for (int k = 0; k < targetDatas.Count; k++) { targetDatas[k].Status.Animetion = skill.TargetAnime; } for (int j = 0; j < targetDatas.Count; j++) { var charaData = actionCharaData[i]; var targetData = targetDatas[j]; DamageCalculation(ref targetData, ref charaData, skill); actionCharaData[i] = charaData; targetDatas[j] = targetData; Logger.write("対象," + i.ToString() + "," + targetData.Parameters.Name, LogType.Debug); } string deathMes = BattleMessageCreator.DeathMessage(targetDatas); string damageMessage = BattleMessageCreator.BattleMessage(targetDatas); battleDataTemp.CommandMessage = targetDatas.FirstOrDefault().Parameters.Name + damageMessage + deathMes; battleDataList.Add(battleDataTemp); //全滅判定 if (battleDataList.Last().CharacterData.Where(p => p.Actor && p.Parameters.HP > 0).Count() == 0 || battleDataList.Last().CharacterData.Where(p => p.Actor == false && p.Parameters.HP > 0).Count() == 0) { extinction = true; break; } } if (extinction) break; } //行動準備初期化(リザルト画面用) var battleDataLast = DeepClone(battleDataList.Last()) as BattleData; //HPが0の場合戦闘不能フラグ battleDataLast.CharacterData.Where(p => p.Parameters.HP <= 0).ToList().ForEach(q => q.Status.Death = true); battleDataLast.CharacterData.ForEach(p => p.Status.Animetion = Animetion.Defalt); battleDataLast.CharacterData.ForEach(p => p.Status.Damage = 0); battleDataList.Add(battleDataLast); return battleDataList; }

 

プロジェクトの中を除く限り、自分で作ったものの中では大規模な部類で、かつ綺麗にまとめられている。

f:id:littlemore:20200526010152p:plain

 

何故、やめてしまったか。

オンラインゲームなのにプレイしてくれるプレイヤーが少なかったこと。

定期的に敵が出現した!みたいな画像を自動でツイートしていたが、それがTwitterの自動化なんとかに引っかかって、アカウントが凍結されてしまったことが要因。

 

ソースコードはあって、アカウントを作れば動かせる。

誰かマネタイズ出来るなら譲渡します。

もちろん、半分くらいもらいます。

 

1年前はクラウドを使うという考えがなかったようで

今で考えたら、Azure使って動かせばよかったなぁと

でも、Gif画像を作るときにすべての画像をメモリで持つからかなりメモリ消費していた気がする・・・。

 

 

説明書もあったので、載せておこう

f:id:littlemore:20200526010655p:plain

f:id:littlemore:20200526010720p:plain

 

C#でのスクレイピングについて

まず、スクレイピングとは何かについて

 

ウェブスクレイピング(英: Web scraping)とは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。 ウェブ・クローラーあるいはウェブ・スパイダーとも呼ばれる。 

引用元:https://ja.wikipedia.org/

 

要は、Webページにある任意の情報を自動で取り出すような技術のこと

スクレイピングについて著作権やら何やら法律絡みのことがあるので、自己責任でお願いします。あと、ちゃんと調べてからやりましょう。

 

C#スクレイピングをするには「AngleSharp」というライブラリを使用した

f:id:littlemore:20200525202615p:plain

 

これさえアレば、あとは楽に出来る

まず、HTMLからIHtmlDocument を作る

/// <summary>
/// IHtmlDocumentを取得します
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public IHtmlDocument GetHtmlDocument(string url)
{
    // 指定したサイトのHTMLをストリームで取得する
    var doc = default(IHtmlDocument);
    try
    {
        System.Threading.Thread.Sleep(1000);

        using (WebClient wc = new WebClient())
        {
            using (Stream st = wc.OpenRead(url))
            using (StreamReader sr = new StreamReader(st))
            {
                string htmlText = sr.ReadToEnd();

                var parser = new HtmlParser();
                doc = parser.ParseDocument(htmlText);
            }
        }
    }
    catch (Exception ex)
    {
        //Logger.Writer(ex.Message);
    }
    return doc;
}

 

あとはこんな感じでCSSセレクターを設定してあげれば、取得したい値が取れる

private static ProductInfo Judge(IHtmlDocument doc, int index)
{
	var info = new ProductInfo();

	// CSSセレクタを指定し取得する
	info.Name = doc?.QuerySelector(string.Format("#sec-02 > ul > li:nth-child({0}) > p.ttl", index))?.InnerHtml?.Trim() ?? string.Empty;
	info.Price = doc?.QuerySelector(string.Format("#sec-02 > ul > li:nth-child({0}) > p.price > span:nth-child(1)", index))?.InnerHtml.Replace("円", string.Empty) ?? string.Empty;
	info.URL = (doc?.QuerySelector(string.Format("#sec-02 > ul > li:nth-child({0}) > p.img > a", index)) as IHtmlAnchorElement)?.Href ?? string.Empty;
	info.Kcal = doc?.QuerySelector(string.Format("#sec-02 > ul > li:nth-child({0}) > p:nth-child(3)", index))?.TextContent ?? string.Empty;
	info.Detail = doc?.QuerySelector(string.Format("#sec-02 > ul > li:nth-child({0}) > p.smalltxt", index))?.InnerHtml?.Trim() ?? string.Empty;

	int i = info.Kcal.IndexOf("kcal");
	if (i >= 0)
		info.Kcal = info.Kcal.Substring(0, i).Trim();

	return info;
}

上のコードだとわかりづらそうなので、一応こちらに書いておく

doc.QuerySelector(【CSSセレクター】)

 

CSSセレクタの取得はここを見るのが分かりやすい

gammasoft.jp

 

コンビニ ガチャについて

【アプリ名】
コンビニ ガチャ
【概要】
ローソンの商品を指定金額内でランダムに選び出すガチャ

 

f:id:littlemore:20200525232446p:plain

コンビニ ガチャ

 

【言語】

C#

【サーバー】

WebAPI:Azure

フロントエンド:Firebase

【DB】

csvファイル

 

お昼ごはんを決めるときや、youtubeの企画、700円くじ等に使えるかもしれません。

(ウェブアプリの説明文からそのまんま)

 

これはC#でWebAPIが試したかったということと、Firebaseでホスティングが出来るということで、処理をC#で行い、Firebaseの方のWebページに返すというウェブアプリです。

 

DB使うまでもないなと思ったのでCSVです。

f:id:littlemore:20200525201146p:plain

 

実装方法も単純でお金がなくなるまで商品を選択していく感じです

public IEnumerable<ProductInfo> SelectRandom(int money)
{
	List<ProductInfo> products = new List<ProductInfo>();
	int nowMoney = money;
	while (true)
	{
		var product = _products.Where(p => p.Price <= nowMoney).OrderBy(i => Guid.NewGuid()).FirstOrDefault();

		if (product == null)
			return products;

		nowMoney -= product.Price;

		products.Add(product);
	}
}

 

ここで一番気になるのが、どうやってコンビニの商品データをCSV化したかどうかというところだと思う

それにはスクレイピングという技術を使った

 

今日はここまで

UUIDガチャについて

【アプリ名】
UUIDガチャ
【概要】
衝突の可能性はほぼないであろうUUIDのガチャ

JavaScriptTest

 

【言語】

javascript

【サーバー】

GitHub(GitHub Pages)

 

 

このアプリはUUIDを使ったガチャだ

 

UUIDとは

UUIDとは、全世界で2つ以上のアイテムが同じ値を持つことがない一意な識別子のこと。何らかの組織やシステムなどが管理・割り当てを行うわけではなく、誰でもいつでも自由に生成することができるが、他のUUIDと重複することは起きないようになっている。

引用元:http://e-words.jp/

要は、一意の値のことだ。

 

 

論理上は当たらないはずだが、当たることもあるらしい。

 

というわけでUUIDを衝突させるためのガチャを作りました。


ソースコードはこれ

至って簡単。最初に比較するUUIDを表示しておき、指定数分UUIDを生成し、比較する!

var objAtari = document.getElementById('win');
var atari = document.createElement('p');
var winUuid = generateUuid();
atari.innerText = winUuid;
objAtari.appendChild(atari);

function lottery() {
    var objA = document.getElementById('emission');
    var getCount = document.getElementById("textbox").value;

    var  win = false;
    for (var i = 0; i < getCount; i++) {
        var objP = document.createElement('li');
        var uuid = generateUuid();
        objP.innerText = uuid;
        objA.appendChild(objP);

        if(uuid == winUuid){
            objP.style.backgroundColor = "red";
            atari.style.backgroundColor = "red";
        }
    }
}

function generateRandTen(){
    return Math.floor(Math.random()*10);
}

function generateUuid() {
    let charArray = "RRRRRRRR-RRRR-4RRR-rRRR-RRRRRRRRRRRR".split("");

    for (let i = 0, len = charArray.length; i < len; i++) {
        switch (charArray[i]) {
            case "R":
                charArray[i] = Math.floor(secureMathRandom() * 16).toString(16);
                break;
            case "r":
                charArray[i] = (Math.floor(secureMathRandom() * 4) + 8).toString(16);
                break;
        }
    }
    return charArray.join("");
}

function secureMathRandom() {
    // 0から1の間の範囲に調整するためにUInt32の最大値(2^32 -1)で割る
    return window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967295;
}

 

こっちにhtmlもあります。

github.com


 

何故javascriptで作ったかと言うと、唯一ブラウザ上で動く言語だということと、業務系の仕事からウェブ系の仕事の会社に転職するときに予習(?)として作ったものなんですね。