مقدمه ای بر مدل پنهان مارکوف با استفاده از پایتون Networkx و Sklearn

مقدمه ای بر مدل پنهان مارکوف با استفاده از پایتون Networkx و Sklearn

رئوس مطالب

  • آندری مارکوف کیست؟
  • ویژگی مارکوفی به کدام ویژگی اطلاق می گردد؟
  • یک مدل مارکوفی چگونه مدلی است؟
  • چه چیز، یک مدل مارکوف را پنهان می سازد؟

آندری مارکوف کیست؟

مارکوف، یک ریاضیدان روسی بود که به خاطر پژوهش درخصوص فرآیندهای تصادفی (Stochastic Processes) شهرت داشت. تمرکز پژوهش اولیه او بر نظریه اعداد (Number Theory) بود، ولی پس از سال 1900، بر نظریه احتمالات (Probability Theory) متمرکز شد، تا آنجا که پس از بازنشستگی رسمی در سال 1950 تا دم مرگ، به آموزش در دوره های آموزشی اشتغال داشت. مارکوف، در طول پژوهش خود توانست قانون اعداد بزرگ (Law of Large Number) و قضیه حد مرکزی (Central Limit Theorem) را جهت کاربرد در دنباله های معینی از متغیرهای تصادفی وابسته، تعمیم دهد که به عنوان زنجیره های مارکوف (Markov Chains) شناخته می شود. زنجیره های مارکوف، کاربرد وسیعی در فیزیک، اقتصاد، آمار، زیست شناسی و غیره دارد. حرکت براونی (Brownian Motion) و ولگشت یا گشت تصادفی (Random Walks)، دو مورد از مشهورترین این کاربردها بودند.

ویژگی مارکوفی (Markov Property) به کدام ویژگی اطلاق می گردد؟

«… یک فرآیند تصادفی که در آن، زمان آینده، با توجه به زمان حال، مستقل از زمان گذشته است.»

یک بازی شیروخط ساده با استفاده از یک سکه سالم را درنظر بگیرید. فرض را بر این گذارید که ویژگی مارکوفی هنوز شناخته شده نیست و ما خواهان پیش بینی احتمال شیر آمدن پس از 10 بار پرتاب سکه هستیم. ما، با توجه به فرض وابستگی مشروط (سکه دارای حافظه حالت های گذشته است و حالت آینده، به دنباله حالت های گذشته وابستگی دارد)، باید دنباله خاصی را ثبت کنیم که به پرتاب 11ام و احتمالات مشترک این پرتاب ها می انجامد. لذا تصور کنید یک دنباله تصادفی شیروخط ها را پس از 10 پرتاب داریم. احتمال مشترک این دنباله 0.0009765625=10^0.5 است. احتمال شیر آمدن در پرتاب بعدی، طبق وابستگی مشروط، 0.00048828125=0.5*0.0009765625 است.

آیا، این، احتمال واقعی شیر آمدن در پرتاب 11ام است؟ هرگز!

ما می دانیم که رویداد پرتاب سکه، به نتیجه پرتاب قبلی وابستگی ندارد، سکه فاقد حافظه است. فرآیند پرتاب های متوالی، تابعی از نتایج قبلی نیست. هر پرتاب، یک رویداد منحصر به فرد با احتمال برابر شیر یا خط است که به آن ویژگی مارکوفی میگویند.

یک مدل مارکوفی چگونه مدلی است؟

یک زنجیره (مدل) مارکوفی، به توصیف یک فرآیند تصادفی می پردازد که در آن، احتمال مفروض حالت (ها)ی آینده، تنها به حالت فرآیند فعلی وابستگی دارد و به هیچ یک از حالت هایی که قبل از آن آمده اند، وابسته نیست.

اجازه دهید به یک مثال ساده بپردازیم. فرض کنید می خواهید این احتمال را مدل سازی کنید که سگ شما با توجه به حالت فعلی اش، در کدام یک از سه حالت خواهد بود. انجام این کار مستلزم تعیین فضای حالت (State Space)، احتمالات [حالت] اولیه (Initial Probabilities) و احتمالات انتقال (Transition Probabilities) است.

این طور تصور کنید که سگ چاق بسیار تنبلی دارید، ازاین رو، فضای حالت را به صورت خوابیدن، خوردن و پی پی کردن تعریف می کنیم. احتمالات [حالت] اولیه را به ترتیب 0.35، 0.35 و 0.30 تعیین می کنیم.

In [59]:
import numpy as np
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
In [60]:
# create state space and initial state probabilities

states = ['sleeping', 'eating', 'pooping']
pi = [0.35, 0.35, 0.3]
state_space = pd.Series(pi, index=states, name='states')
print(state_space)
print(state_space.sum())
sleeping    0.35
eating      0.35
pooping     0.30
Name: states, dtype: float64
1.0

تعریف احتمالات انتقال، گام بعدی را تشکیل می دهد. این احتمالات، صرفا احتمالات ماندن در یک حالت یا انتقال به یک حالت متفاوت را با توجه به حالت فعلی دربر می گیرند.

In [61]:
# create transition matrix
# equals transition probability matrix of changing states given a state
# matrix is size (M x M) where M is number of states

q_df = pd.DataFrame(columns=states, index=states)
q_df.loc[states[0]] = [0.4, 0.2, 0.4]
q_df.loc[states[1]] = [0.45, 0.45, 0.1]
q_df.loc[states[2]] = [0.45, 0.25, .3]

print(q_df)

q = q_df.values
print('\n', q, q.shape, '\n')
print(q_df.sum(axis=1))
         sleeping eating pooping
sleeping      0.4    0.2     0.4
eating       0.45   0.45     0.1
pooping      0.45   0.25     0.3

 [[0.4 0.2 0.4]
 [0.45 0.45 0.1]
 [0.45 0.25 0.3]] (3, 3) 

sleeping    1.0
eating      1.0
pooping     1.0
dtype: float64

اکنون که مجموعه احتمالات اولیه و انتقال را در اختیار داریم، یک نمودار مارکوف را با استفاده از بسته Networkx می توانیم ایجاد نماییم.

این کار مستلزم اندکی تفکر انعطاف پذیر است. Networkx، گراف هایی ایجاد می کند که از گره ها و یال ها تشکیل می شود. در مثال ما، حالت های ممکن سگ، گره ها و خطوط متصل کننده ی گره ها، یال ها هستند. وزن یال ها را احتمال انتقالات قرار میدهیم. آن ها، احتمال انتقال به یک حالت را با توجه به حالت فعلی نشان می دهند.

لازم به ذکر است که Networkx، اساسا با دیکشنری ها کار میکند. با این وصف، باید یک دیکشنری ایجاد کنیم که شامل یال های ما و اوزان مربوط به آن ها باشد.

In [62]:
from pprint import pprint, pformat

# create a function that maps transition probability dataframe 
# to markov edges and weights

def _get_markov_edges(Q):
    edges = {}
    for col in Q.columns:
        for idx in Q.index:
            edges[(idx,col)] = Q.loc[idx,col]
    return edges

edges_wts = _get_markov_edges(q_df)
pprint(edges_wts)
{('eating', 'eating'): 0.45,
 ('eating', 'pooping'): 0.1,
 ('eating', 'sleeping'): 0.45,
 ('pooping', 'eating'): 0.25,
 ('pooping', 'pooping'): 0.3,
 ('pooping', 'sleeping'): 0.45,
 ('sleeping', 'eating'): 0.2,
 ('sleeping', 'pooping'): 0.4,
 ('sleeping', 'sleeping'): 0.4}

ما، اکنون، می توانیم این گراف را ایجاد نماییم. nx.MultiDiGraph را جهت ساخت یک مدل مارکوف باید مورد استفاده قرار دهیم. یک Multidigraph، یک گراف جهت دار (Directed Graph) است که قوس های متعددی می تواند داشته باشد، بدین صورت که هر گره، می تواند هم مبدا و هم مقصد باشد.

ما، در کد قسمت زیر، این گراف را ایجاد می کنیم، گره ها، لبه ها و برچسب های خود را اضافه می نماییم، سپس گراف را ترسیم میکنیم.

In [63]:
# create graph object
G = nx.MultiDiGraph()

# nodes correspond to states
G.add_nodes_from(states)
print(f'Nodes:\n{G.nodes()}\n')

# edges represent transition probabilities
for k, v in edges_wts.items():
    tmp_origin, tmp_destination = k[0], k[1]
    G.add_edge(tmp_origin, tmp_destination, weight=v, label=v)
print(f'Edges:')
pprint(list(G.edges(data=True)))

pos = nx.drawing.nx_pydot.graphviz_layout(G, prog='dot')
nx.draw_networkx(G, pos)

# create edge labels for jupyter plot but is not necessary
edge_labels = {(n1,n2):d['label'] for n1,n2,d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G , pos, edge_labels=edge_labels)
Nodes:
['sleeping', 'eating', 'pooping']

Edges:
[('sleeping', 'sleeping', {'label': 0.4, 'weight': 0.4}),
 ('sleeping', 'eating', {'label': 0.2, 'weight': 0.2}),
 ('sleeping', 'pooping', {'label': 0.4, 'weight': 0.4}),
 ('eating', 'sleeping', {'label': 0.45, 'weight': 0.45}),
 ('eating', 'eating', {'label': 0.45, 'weight': 0.45}),
 ('eating', 'pooping', {'label': 0.1, 'weight': 0.1}),
 ('pooping', 'sleeping', {'label': 0.45, 'weight': 0.45}),
 ('pooping', 'eating', {'label': 0.25, 'weight': 0.25}),
 ('pooping', 'pooping', {'label': 0.3, 'weight': 0.3})]
Out[63]:
{('sleeping', 'sleeping'): Text(58.147, 192.0, '0.4'),
 ('sleeping', 'eating'): Text(45.647, 148.5, '0.2'),
 ('sleeping', 'pooping'): Text(58.147, 105.0, '0.4'),
 ('eating', 'sleeping'): Text(45.647, 148.5, '0.45'),
 ('eating', 'eating'): Text(33.147, 105.0, '0.45'),
 ('eating', 'pooping'): Text(45.647, 61.5, '0.1'),
 ('pooping', 'sleeping'): Text(58.147, 105.0, '0.45'),
 ('pooping', 'eating'): Text(45.647, 61.5, '0.25'),
 ('pooping', 'pooping'): Text(58.147, 18.0, '0.3')}

حال نگاهی به فایل dot داریم.

In [74]:
from IPython.display import Image, display, SVG
display(Image(nx.drawing.nx_pydot.to_pydot(G).create_png()))

بد نیست. چنانچه لبه های هر گره را دنبال کنید، احتمال انتقال سگ به حالت دیگر را به شما خواهد گفت. برای نمونه، اگر سگ خواب باشد، احتمال ادامه خواب 0.40، احتمال بیدار شدن و پی پی کردن 0.40 و احتمال بیدار شدن و غذا خوردن 0.20 قابل مشاهده است.

چه چیز، یک Markov Model Hidden را می سازد؟

وضعیتی را درنظر بگیرید که در آن، سگ شما، رفتار عجیب و غریبی دارد و شما خواهان مدل سازی این احتمال هستید که این رفتار سگ شما، به علت بیماری است یا صرفا عادت رفتاری عجیبی است که در غیر این صورت، رفتار سالم محسوب می گردد.

حالت واقعی سگ در این وضعیت، معلوم نیست، ازاین رو، از شما پنهان است. یک راه مدل سازی این حالت، این است که فرض کنید سگ دارای رفتارهای قابل مشاهده ای است که حالت پنهان واقعی را نشان می دهد. اجازه دهید مثالی بزنیم.

ما، ابتدا، فضای حالت خود سالم یا بیمار- را ایجاد می کنیم. احتمال آن ها را برابر درنظر می گیریم.

In [65]:
# create state space and initial state probabilities

hidden_states = ['healthy', 'sick']
pi = [0.5, 0.5]
state_space = pd.Series(pi, index=hidden_states, name='states')
print(state_space)
print('\n', state_space.sum())
healthy    0.5
sick       0.5
Name: states, dtype: float64

 1.0

آنگاه ماتریس انتقال حالت های پنهان خود را ایجاد می نماییم.

In [66]:
# create hidden transition matrix
# a or alpha 
#   = transition probability matrix of changing states given a state
# matrix is size (M x M) where M is number of states

a_df = pd.DataFrame(columns=hidden_states, index=hidden_states)
a_df.loc[hidden_states[0]] = [0.7, 0.3]
a_df.loc[hidden_states[1]] = [0.4, 0.6]

print(a_df)

a = a_df.values
print('\n', a, a.shape, '\n')
print(a_df.sum(axis=1))
        healthy sick
healthy     0.7  0.3
sick        0.4  0.6

 [[0.7 0.3]
 [0.4 0.6]] (2, 2) 

healthy    1.0
sick       1.0
dtype: float64

حال، زمانی است که به قدری توجه بیشتر نیاز دارد. ماتریس احتمال انتشار (Emission) یا مشاهده را ایجاد می کنیم. این ماتریس دارای اندازه M×O است که M، تعداد حالت های پنهان و O، تعداد حالت های قابل مشاهده ممکن است.

ماتریس انتشار، احتمال بودن سگ در یکی از حالت های پنهان را باتوجه به حالت قابل مشاهده فعلی، بیان می کند.

حال بیایید همان حالت های قابل مشاهده نمونه پیشین را درنظر بگیریم. سگ می تواند بخوابد، بخورد یا پی پی کند. ما، اکنون، بهترین حدس خود را برای تکمیل احتمالات می زنیم.

In [67]:
# create matrix of observation (emission) probabilities
# b or beta = observation probabilities given state
# matrix is size (M x O) where M is number of states 
# and O is number of different possible observations

observable_states = states

b_df = pd.DataFrame(columns=observable_states, index=hidden_states)
b_df.loc[hidden_states[0]] = [0.2, 0.6, 0.2]
b_df.loc[hidden_states[1]] = [0.4, 0.1, 0.5]

print(b_df)

b = b_df.values
print('\n', b, b.shape, '\n')
print(b_df.sum(axis=1))
        sleeping eating pooping
healthy      0.2    0.6     0.2
sick         0.4    0.1     0.5

 [[0.2 0.6 0.2]
 [0.4 0.1 0.5]] (2, 3) 

healthy    1.0
sick       1.0
dtype: float64

حال، لبه ها و اوزان گراف را ایجاد می نماییم.

In [68]:
# create graph edges and weights

hide_edges_wts = _get_markov_edges(a_df)
pprint(hide_edges_wts)

emit_edges_wts = _get_markov_edges(b_df)
pprint(emit_edges_wts)
{('healthy', 'healthy'): 0.7,
 ('healthy', 'sick'): 0.3,
 ('sick', 'healthy'): 0.4,
 ('sick', 'sick'): 0.6}
{('healthy', 'eating'): 0.6,
 ('healthy', 'pooping'): 0.2,
 ('healthy', 'sleeping'): 0.2,
 ('sick', 'eating'): 0.1,
 ('sick', 'pooping'): 0.5,
 ('sick', 'sleeping'): 0.4}
In [69]:
# create graph object
G = nx.MultiDiGraph()

# nodes correspond to states
G.add_nodes_from(hidden_states)
print(f'Nodes:\n{G.nodes()}\n')

# edges represent hidden probabilities
for k, v in hide_edges_wts.items():
    tmp_origin, tmp_destination = k[0], k[1]
    G.add_edge(tmp_origin, tmp_destination, weight=v, label=v)

# edges represent emission probabilities
for k, v in emit_edges_wts.items():
    tmp_origin, tmp_destination = k[0], k[1]
    G.add_edge(tmp_origin, tmp_destination, weight=v, label=v)
    
print(f'Edges:')
pprint(list(G.edges(data=True)))

pos = nx.drawing.nx_pydot.graphviz_layout(G, prog='neato')
nx.draw_networkx(G, pos)

# create edge labels for jupyter plot but is not necessary
emit_edge_labels = {(n1,n2):d['label'] for n1,n2,d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G , pos, edge_labels=emit_edge_labels)
nx.drawing.nx_pydot.write_dot(G, 'pet_dog_hidden_markov.dot')
Nodes:
['healthy', 'sick']

Edges:
[('healthy', 'healthy', {'label': 0.7, 'weight': 0.7}),
 ('healthy', 'sick', {'label': 0.3, 'weight': 0.3}),
 ('healthy', 'sleeping', {'label': 0.2, 'weight': 0.2}),
 ('healthy', 'eating', {'label': 0.6, 'weight': 0.6}),
 ('healthy', 'pooping', {'label': 0.2, 'weight': 0.2}),
 ('sick', 'healthy', {'label': 0.4, 'weight': 0.4}),
 ('sick', 'sick', {'label': 0.6, 'weight': 0.6}),
 ('sick', 'sleeping', {'label': 0.4, 'weight': 0.4}),
 ('sick', 'eating', {'label': 0.1, 'weight': 0.1}),
 ('sick', 'pooping', {'label': 0.5, 'weight': 0.5})]
In [75]:
display(Image(nx.drawing.nx_pydot.to_pydot(G).create_png()))

مدل پنهان مارکوف، کمی پیچیده تر است، اما اصول یکی است. برای مثال، چنانچه سگ غذا می خورد، انتظار دارید یک احتمال بالای سالم بودن (60%) و احتمال پایین بیمار بودن (10%) وجود داشته باشد.

اکنون، اگر تشخیص سلامت سگ در طول زمان، با توجه به یک دنباله مشاهدات، مورد نیاز باشد، چی؟

In [71]:
# observation sequence of dog's behaviors
# observations are encoded numerically

obs_map = {'sleeping':0, 'eating':1, 'pooping':2}
obs = np.array([1,1,2,1,0,1,2,1,0,2,2,0,1,0,1])

inv_obs_map = dict((v,k) for k, v in obs_map.items())
obs_seq = [inv_obs_map[v] for v in list(obs)]

print( pd.DataFrame(np.column_stack([obs, obs_seq]), 
                columns=['Obs_code', 'Obs_seq']) )
   Obs_code   Obs_seq
0         1    eating
1         1    eating
2         2   pooping
3         1    eating
4         0  sleeping
5         1    eating
6         2   pooping
7         1    eating
8         0  sleeping
9         2   pooping
10        2   pooping
11        0  sleeping
12        1    eating
13        0  sleeping
14        1    eating

ما، با استفاده از الگوریتم Viterbi، محتمل ترین دنباله حالت های پنهان را با توجه به این دنباله مشاهدات می توانیم شناسایی نماییم.

به صورت کلی الگوریتم Viterbi، در طول هر گام زمانی نمو پیدا می کند (Increment) که بیشینه احتمال هر مسیر (Path) منتهی شونده به حالت i در زمان t را مشخص می سازد و در ضمن، دارای مشاهدات صحیح برای این دنباله تا زمان t است.

این الگوریتم، همچنین، رد (Track) حالت دارای بالاترین احتمال در هر مرحله را نگه می دارد. این الگوریتم، در پایان این دنباله، به عقب برمی گردد و به انتخاب حالتی می پردازد که «برنده» (Won) هر گام زمانی بود و از این رو، محتمل ترین مسیر یا دنباله احتمالی حالت های پنهان را ایجاد می کند که به دنباله مشاهدات منتهی می گردد.

In [72]:
# define Viterbi algorithm for shortest path
# code adapted from Stephen Marsland's, Machine Learning An Algorthmic Perspective, Vol. 2
# https://github.com/alexsosn/MarslandMLAlgo/blob/master/Ch16/HMM.py

def viterbi(pi, a, b, obs):
    
    nStates = np.shape(b)[0]
    T = np.shape(obs)[0]
    
    # init blank path
    path = np.zeros(T)
    # delta --> highest probability of any path that reaches state i
    delta = np.zeros((nStates, T))
    # phi --> argmax by time step for each state
    phi = np.zeros((nStates, T))
    
    # init delta and phi 
    delta[:, 0] = pi * b[:, obs[0]]
    phi[:, 0] = 0

    print('\nStart Walk Forward\n')    
    # the forward algorithm extension
    for t in range(1, T):
        for s in range(nStates):
            delta[s, t] = np.max(delta[:, t-1] * a[:, s]) * b[s, obs[t]] 
            phi[s, t] = np.argmax(delta[:, t-1] * a[:, s])
            print('s={s} and t={t}: phi[{s}, {t}] = {phi}'.format(s=s, t=t, phi=phi[s, t]))
    
    # find optimal path
    print('-'*50)
    print('Start Backtrace\n')
    path[T-1] = np.argmax(delta[:, T-1])
#     print('init path\n    t={} path[{}-1]={}\n'.format(T-1, T, path[T-1]))
    for t in range(T-2, -1, -1):
        path[t] = phi[int(path[t+1]), t+1]
        #p(' '*4 + 't={t}, path[{t}+1]={path}, [{t}+1]={i}'.format(t=t, path=path[t+1], i=[t+1]))
        print('path[{}] = {}'.format(t, path[t]))
        
    return path, delta, phi

path, delta, phi = viterbi(pi, a, b, obs)
print('\nsingle best state path: \n', pformat(path))
print('delta:\n', pformat(delta))
print('phi:\n', pformat(phi))
Start Walk Forward

s=0 and t=1: phi[0, 1] = 0.0
s=1 and t=1: phi[1, 1] = 0.0
s=0 and t=2: phi[0, 2] = 0.0
s=1 and t=2: phi[1, 2] = 0.0
s=0 and t=3: phi[0, 3] = 0.0
s=1 and t=3: phi[1, 3] = 1.0
s=0 and t=4: phi[0, 4] = 0.0
s=1 and t=4: phi[1, 4] = 0.0
s=0 and t=5: phi[0, 5] = 0.0
s=1 and t=5: phi[1, 5] = 1.0
s=0 and t=6: phi[0, 6] = 0.0
s=1 and t=6: phi[1, 6] = 0.0
s=0 and t=7: phi[0, 7] = 0.0
s=1 and t=7: phi[1, 7] = 1.0
s=0 and t=8: phi[0, 8] = 0.0
s=1 and t=8: phi[1, 8] = 0.0
s=0 and t=9: phi[0, 9] = 0.0
s=1 and t=9: phi[1, 9] = 1.0
s=0 and t=10: phi[0, 10] = 1.0
s=1 and t=10: phi[1, 10] = 1.0
s=0 and t=11: phi[0, 11] = 1.0
s=1 and t=11: phi[1, 11] = 1.0
s=0 and t=12: phi[0, 12] = 1.0
s=1 and t=12: phi[1, 12] = 1.0
s=0 and t=13: phi[0, 13] = 0.0
s=1 and t=13: phi[1, 13] = 0.0
s=0 and t=14: phi[0, 14] = 0.0
s=1 and t=14: phi[1, 14] = 1.0
--------------------------------------------------
Start Backtrace

path[13] = 0.0
path[12] = 0.0
path[11] = 1.0
path[10] = 1.0
path[9] = 1.0
path[8] = 1.0
path[7] = 0.0
path[6] = 0.0
path[5] = 0.0
path[4] = 0.0
path[3] = 0.0
path[2] = 0.0
path[1] = 0.0
path[0] = 0.0

single best state path: 
 array([0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 0., 0., 0.])
delta:
 array([[3.00000000e-01, 1.26000000e-01, 1.76400000e-02, 7.40880000e-03,
        1.03723200e-03, 4.35637440e-04, 6.09892416e-05, 2.56154815e-05,
        3.58616741e-06, 5.02063437e-07, 7.37725866e-08, 2.21317760e-08,
        1.59348787e-08, 2.23088302e-09, 9.36970868e-10],
       [5.00000000e-02, 9.00000000e-03, 1.89000000e-02, 1.13400000e-03,
        8.89056000e-04, 5.33433600e-05, 6.53456160e-05, 3.92073696e-06,
        3.07385778e-06, 9.22157333e-07, 2.76647200e-07, 6.63953280e-08,
        3.98371968e-09, 1.91218545e-09, 1.14731127e-10]])
phi:
 array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0.],
       [0., 0., 0., 1., 0., 1., 0., 1., 0., 1., 1., 1., 1., 0., 1.]])

حال بیایید به نتایج نگاهی بیندازیم.

In [73]:
state_map = {0:'healthy', 1:'sick'}
state_path = [state_map[v] for v in path]

(pd.DataFrame()
 .assign(Observation=obs_seq)
 .assign(Best_Path=state_path))
Out[73]:
ObservationBest_Path
0eatinghealthy
1eatinghealthy
2poopinghealthy
3eatinghealthy
4sleepinghealthy
5eatinghealthy
6poopinghealthy
7eatinghealthy
8sleepingsick
9poopingsick
10poopingsick
11sleepingsick
12eatinghealthy
13sleepinghealthy
14eatinghealthy

منبع: blackarbs.com

آیا سوالی دارید؟

سوال خود را در بخش نظرات بپرسید، سعی میکنم به آنها جواب مناسب بدم.

دیدگاهتان را بنویسید