10-2. 추천시스템(optimizer 사용 고급, MF-based 추천시스템)

Author

이상민

Published

May 12, 2025

1. imports

import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (4.5, 3.0)

2. 예비학습

A. optimizer 사용 고급

- 회귀분석

  • 아래의 주어진 자로가 있다
torch.manual_seed(43052)
x,_ = torch.randn(100).sort()
x = x.reshape(-1,1)
ones= torch.ones(100).reshape(-1,1)
X = torch.concat([ones,x],axis=-1)
ϵ = torch.randn(100).reshape(-1,1)*0.5
y = 2.5+ 4*x + ϵ
plt.plot(x,y,'o')

w = torch.tensor(10.0,requires_grad=True)
b = torch.tensor(-5.0,requires_grad=True)
plt.plot(x,y,'o')
plt.plot(x,(x*w + b).data,'--')

- torch.optim.SGD 를 이용하여 What 을 update하라. 학습률은 0.1로 설정하고 30회 update하라.

# net  
loss_fn = torch.nn.MSELoss()
optimizr = torch.optim.SGD([w,b],lr=0.1)
#--#
for epoc in range(30):
    # step1 
    yhat = x*w+b 
    # step2
    loss = loss_fn(yhat,y)
    # step3 
    loss.backward()
    # step4 
    optimizr.step()
    optimizr.zero_grad()
plt.plot(x,y,'o')
plt.plot(x,(x*w + b).data,'--')

- 중간고사 3번

torch.manual_seed(43052)
dist = torch.distributions.Exponential(1/2)
x = dist.sample((10000,1))

주어진 자료 \(x_i\)에 대하여 함수 \(l(\lambda)\)를 최대화하는 \(\lambda\)를 경사하강법 기반의 알고리즘을 이용하여 추정하라. 단 이때 \(\lambda\)의 초기 추정값은 1로 설정하라.

\[ l(\lambda) =\frac{1}{n} \sum_{i=1}^{n}\log f(x_i), \quad f(x_i) = \frac{1}{\lambda} e^{-\frac{x_i}{\lambda}}, \quad x_i \geq 0 \]

hint

  • \(l(\lambda)\)를 최대화하는 \(\lambda\)\(-l(\lambda)\)를 최소화합니다.
  • 이론적으로는 \(l(\lambda)\)를 최대화하는 \(\lambda\)x.mean()입니다. 즉 제대로 \(\lambda\)를 추정한다면 x.mean()이 나오도록 되어있습니다.
  • 저는 경사하강법을 이용했고 학습률은 0.05로 설정했습니다. 1000회 update하니까 잘 수렴했습니다.

(풀이1)

lamb = torch.tensor([[1.0]],requires_grad=True)
for i in range(1000):
    fx = torch.exp(-x/lamb)/lamb
    l = - torch.log(fx).mean()
    l.backward()
    lamb.data = lamb.data - 0.05 * lamb.grad
    lamb.grad = None 
lamb
tensor([[1.9874]], requires_grad=True)

(풀이2)

lamb = torch.tensor([[1.0]],requires_grad=True)
optimizr = torch.optim.SGD([lamb],lr=0.1)
for i in range(1000):
    fx = torch.exp(-x/lamb)/lamb
    l = - torch.log(fx).mean()
    l.backward()
    optimizr.step()
    optimizr.zero_grad()
lamb
tensor([[1.9874]], requires_grad=True)

B. 모델링 전략

- 2025-중간고사 4번` – 자유 낙하 운동이란 어떤 물체가 일정한 높이에서 떨어져 지면에 도달하기 까지 걸리는 시간을 다루는 물리학 개념이다. 다음은 물리학의 자유 낙하 운동에서 착안하여 생성한 데이터이다.

torch.manual_seed(43052)
h = torch.rand(100)*100
h,_ = h.sort()
h = h.reshape(100,1)
t = torch.sqrt(2*h/9.8) + torch.randn([100,1])*0.1

여기에서 \(h\)는 낙하전의 높이(단위: m), \(t\)는 해당높이에서 물치가 지면에 도달하기 까지 걸리는 시간(단위:초)을 의미한다. 예를 들어 아래의 자료는 \(h=99.3920, t=4.4583\)를 의미하는데

h[-1], t[-1]
(tensor([99.3920]), tensor([4.4583]))

이것은 높이 \(99.3920\)m에서 낙하한 물체가 약 \(4.4583\)초만에 지면에 도달했음을 의미한다. 아래의 그림은 \(x\)축에 \(h\), \(y\)축에 \(t\)를 두고 해당 데이터를 산점도로 시각화 한 것이다.

plt.plot(h,t,'o',alpha=0.5)
plt.xlabel('Height (m)')
plt.ylabel('Time to fall (sec)')
plt.title('Free Fall Time vs Height')
plt.grid(True)
plt.show()

그래프를 보면 높이가 높을 수록 낙하시간도 길어지는 경향이 관찰된다. 다만 동일한 높이라 하더라도 낙하시간이 조금씩 차이나는 경우가 있는데, 이는 사람이 시간측정을 수동으로 하며 발생하는 실험오차 때문이다. 이러한 오차에도 불구하고 \(h\)\(t\)사이에는 일정한 규칙이 존재하는듯 하다. 물리학과 교수님께 자문을 요청한 결과 자유낙하에 걸리는 시간은 \(\sqrt{h}\)에 비례함을 알 수 있었고 이를 근거로 아래와 같은 모형을 설계하였다.

\[t_i = \beta_0 + \beta_1 \sqrt{h_i}+\epsilon_i, \quad \epsilon_i \sim {\cal N}(0,\sigma^2)\]

위의 모형을 활용하여 높이 \(h\)로부터 낙하시간 \(t\)를 예측하는 신경망 모델을 설계하고 학습하라. 학습한 신경망 모델을 활용하여 높이 40m,60m,80m 에서 물체를 자유낙하 시켰을때 지면에 도달하기까지 걸리는 시간을 각각 예측하라.

hint

  • \(y_i = t_i\) 로 생각하시고 \(x_i= \sqrt{h}_i\)로 생각하시면 그냥 회귀모형이죠?
  • 답은 \(2.8571\)초, \(3.4493\)초, \(4.0406\)초 근처로 나오면 됩니다.
  • 제시된 모형(\(t_i = \beta_0 + \beta_1 \sqrt{h_i}+\epsilon_i\))을 무시하고 04wk-2와 같은 방식으로 신경망을 설계하고 푸셔도 만점으로 인정합니다.

(풀이1)

net = torch.nn.Sequential(
    torch.nn.Linear(1,32),
    torch.nn.ReLU(),
    torch.nn.Linear(32,1)
)
optimizr = torch.optim.Adam(net.parameters())
loss_fn = torch.nn.MSELoss()
#---#
for epoc in range(1000):
    ##1
    that = net(h)
    ##2
    loss = loss_fn(that,t)
    ##3
    loss.backward()
    ##4
    optimizr.step()
    optimizr.zero_grad()
plt.plot(h,t,'o',alpha=0.5)
plt.xlabel('Height (m)')
plt.ylabel('Time to fall (sec)')
plt.title('Free Fall Time vs Height')
plt.grid(True)
plt.plot(h,that.data, '--')
plt.show()

hh = torch.tensor([40,60,80]).float().reshape(3,1)
net(hh)
tensor([[2.7926],
        [3.3998],
        [4.0070]], grad_fn=<AddmmBackward0>)

- 풀이1 net 사용 x

linr1 = torch.nn.Linear(1,32)
relu = torch.nn.ReLU()
linr2 =torch.nn.Linear(32,1)
optimizr = torch.optim.Adam(list(linr1.parameters())+list(linr2.parameters()))
loss_fn = torch.nn.MSELoss()
#---#
for epoc in range(2000):
    ##1
    that = linr2(relu(linr1(h)))
    ##2
    loss = loss_fn(that,t)
    ##3
    loss.backward()
    ##4
    optimizr.step()
    optimizr.zero_grad()
plt.plot(h,t,'o',alpha=0.5)
plt.xlabel('Height (m)')
plt.ylabel('Time to fall (sec)')
plt.title('Free Fall Time vs Height')
plt.grid(True)
plt.plot(h,that.data, '--')
plt.show()

(풀이2)

x = torch.sqrt(h)
y = t 
net = torch.nn.Sequential(
    torch.nn.Linear(1,1)
)
optimizr = torch.optim.Adam(net.parameters())
loss_fn = torch.nn.MSELoss()
#---#
for epoc in range(1000):
    ##1
    yhat = net(x)
    ##2
    loss = loss_fn(yhat,y)
    ##3
    loss.backward()
    ##4
    optimizr.step()
    optimizr.zero_grad()
plt.plot(h,t,'o',alpha=0.5)
plt.xlabel('Height (m)')
plt.ylabel('Time to fall (sec)')
plt.title('Free Fall Time vs Height')
plt.grid(True)
plt.plot(x**2,yhat.data, '--')
plt.show()

xx = torch.sqrt(torch.tensor([40,60,80])).float().reshape(3,1)
net(xx)
tensor([[0.7239],
        [0.6690],
        [0.6228]], grad_fn=<AddmmBackward0>)

3. MF-based 추천시스템

A. Data : 나는 SOLO

- Data

df_view = pd.read_csv('https://raw.githubusercontent.com/guebin/DL2025/main/posts/iamsolo.csv',index_col=0)
df_view
영식(IN) 영철(IN) 영호(IS) 광수(IS) 상철(EN) 영수(EN) 규빈(ES) 다호(ES)
옥순(IN) NaN 4.02 3.45 3.42 0.84 1.12 0.43 0.49
영자(IN) 3.93 3.99 3.63 3.43 0.98 0.96 0.52 NaN
정숙(IS) 3.52 3.42 4.05 4.06 0.39 NaN 0.93 0.99
영숙(IS) 3.43 3.57 NaN 3.95 0.56 0.52 0.89 0.89
순자(EN) 1.12 NaN 0.59 0.43 4.01 4.16 3.52 3.38
현숙(EN) 0.94 1.05 0.32 0.45 4.02 3.78 NaN 3.54
서연(ES) 0.51 0.56 0.88 0.89 3.50 3.64 4.04 4.10
보람(ES) 0.48 0.51 1.03 NaN 3.52 4.00 3.82 NaN
하니(I) 4.85 4.82 NaN 4.98 4.53 4.39 4.45 4.52

- 데이터를 이해할 때 필요한 가정들

  • 궁합이 잘맞으면 5점, 잘 안맞으면 0점
  • MBTI 성향에 따라서 궁함의 정도가 다름. 특히 I/E의 성향일치가 중요
  • 하니는 모든 사람들과 대체로 궁합이 잘 맞는다.

B. 아이디어

- 목표: NaN을 추정

- 수동추론: 그럴듯한 숫자를 추정해보자.

  • 옥순(IN),영식(IN)의 궁합은? \(\to\) 잘 맞을듯
  • 서연(ES),규빈(ES)의 궁합은? \(\to\) 잘 맞을듯
  • 영자(IN),다호(ES)의 궁합은? \(\to\) 잘 안맞을듯
  • 하니(I),영호(IS)의 궁합은? \(\to\)엄청 잘 맞을듯

- 좀 더 체계적인 추론 전략

sig = torch.nn.Sigmoid()

(1) 옥순(IN)과 영식(IN)의 궁합 \(\approx\) 옥순의I/E성향\(\times\)영식의I/E성향 \(+\) 옥순의N/S성향\(\times\)영식의N/S성향

옥순성향 = torch.tensor([1.9, 1.9]).reshape(1,2)
영식성향 = torch.tensor([1.9,1.9]).reshape(1,2)
sig(((옥순성향*영식성향).sum()))*5
sig((옥순성향@영식성향.T))*5
tensor([[4.9963]])

(2) 서연(ES)과 규빈(ES)의 궁합 \(\approx\) 서연의I/E성향\(\times\)규빈의I/E성향 \(+\) 서연의N/S성향\(\times\)규빈의N/S성향

서연성향 = torch.tensor([-1.9, -1.9]).reshape(1,2)
규빈성향 = torch.tensor([-1.9,-1.9]).reshape(1,2)
sig(((서연성향*규빈성향).sum()))*5
sig((서연성향@규빈성향.T))*5
tensor([[4.9963]])

(3) 영자(IN)와 다호(ES)의 궁합 \(\approx\) 영자I/E성향\(\times\)다호I/E성향 \(+\) 영자N/S성향\(\times\)다호의N/S성향

영자성향 = torch.tensor([1.9,1.9]).reshape(1,2)
다호성향 = torch.tensor([-1.9,-1.9]).reshape(1,2)
sig(((영자성향*다호성향).sum()))*5
sig((영자성향@다호성향.T))*5
tensor([[0.0037]])

(4) 하니(I)와 영호(IS)의 궁합 \(\approx\) 하니I/E성향\(\times\)영호I/E성향 \(+\) 하니N/S성향\(\times\)영호의N/S성향 \(+\) 하니의매력

하니성향 = torch.tensor([2.0, 0]).reshape(1,2)
하니매력 = torch.tensor(5)
영호성향 = torch.tensor([2.0,-2.0]).reshape(1,2)
영호매력 = torch.tensor(0)
sig(((하니성향*영호성향).sum() + 하니매력))*5
sig((하니성향@영호성향.T + 하니매력))*5
tensor([[4.9994]])

- \(\to\) 전체적으로 그럴싸함

- 전체 사용자의 설정값

옥순성향 = torch.tensor([1.8,1])
영자성향 = torch.tensor([1.8,1])
정숙성향 = torch.tensor([1.8,-1])
영숙성향 = torch.tensor([1.8,-1])
순자성향 = torch.tensor([-1.8,1])
현숙성향 = torch.tensor([-1.8,1])
서연성향 = torch.tensor([-1.8,-1])
보람성향 = torch.tensor([-1.8,-1])
하니성향 = torch.tensor([1.8,0])
W = torch.stack([옥순성향,영자성향,정숙성향,영숙성향,순자성향,현숙성향,서연성향,보람성향,하니성향])
b1 = torch.tensor([0,0,0,0,0,0,0,0,5]).reshape(-1,1) 
W,b1
(tensor([[ 1.8000,  1.0000],
         [ 1.8000,  1.0000],
         [ 1.8000, -1.0000],
         [ 1.8000, -1.0000],
         [-1.8000,  1.0000],
         [-1.8000,  1.0000],
         [-1.8000, -1.0000],
         [-1.8000, -1.0000],
         [ 1.8000,  0.0000]]),
 tensor([[0],
         [0],
         [0],
         [0],
         [0],
         [0],
         [0],
         [0],
         [5]]))
영식성향 = torch.tensor([1.8,1])
영철성향 = torch.tensor([1.8,1])
영호성향 = torch.tensor([1.8,-1])
광수성향 = torch.tensor([1.8,-1])
상철성향 = torch.tensor([-1.8,1])
영수성향 = torch.tensor([-1.8,1])
규빈성향 = torch.tensor([-1.8,-1])
다호성향 = torch.tensor([-1.8,-1])
M = torch.stack([영식성향,영철성향,영호성향,광수성향,상철성향,영수성향,규빈성향,다호성향]) # 각 column은 남성출연자의 성향을 의미함
b2 = torch.zeros(8).reshape(-1,1)
M,b2
(tensor([[ 1.8000,  1.0000],
         [ 1.8000,  1.0000],
         [ 1.8000, -1.0000],
         [ 1.8000, -1.0000],
         [-1.8000,  1.0000],
         [-1.8000,  1.0000],
         [-1.8000, -1.0000],
         [-1.8000, -1.0000]]),
 tensor([[0.],
         [0.],
         [0.],
         [0.],
         [0.],
         [0.],
         [0.],
         [0.]]))

- 아래의 행렬곱 관찰

sig(W @ M.T + (b1 + b2.T))*5
tensor([[4.9290, 4.9290, 4.5189, 4.5189, 0.4811, 0.4811, 0.0710, 0.0710],
        [4.9290, 4.9290, 4.5189, 4.5189, 0.4811, 0.4811, 0.0710, 0.0710],
        [4.5189, 4.5189, 4.9290, 4.9290, 0.0710, 0.0710, 0.4811, 0.4811],
        [4.5189, 4.5189, 4.9290, 4.9290, 0.0710, 0.0710, 0.4811, 0.4811],
        [0.4811, 0.4811, 0.0710, 0.0710, 4.9290, 4.9290, 4.5189, 4.5189],
        [0.4811, 0.4811, 0.0710, 0.0710, 4.9290, 4.9290, 4.5189, 4.5189],
        [0.0710, 0.0710, 0.4811, 0.4811, 4.5189, 4.5189, 4.9290, 4.9290],
        [0.0710, 0.0710, 0.4811, 0.4811, 4.5189, 4.5189, 4.9290, 4.9290],
        [4.9987, 4.9987, 4.9987, 4.9987, 4.2660, 4.2660, 4.2660, 4.2660]])
df_view
영식(IN) 영철(IN) 영호(IS) 광수(IS) 상철(EN) 영수(EN) 규빈(ES) 다호(ES)
옥순(IN) NaN 4.02 3.45 3.42 0.84 1.12 0.43 0.49
영자(IN) 3.93 3.99 3.63 3.43 0.98 0.96 0.52 NaN
정숙(IS) 3.52 3.42 4.05 4.06 0.39 NaN 0.93 0.99
영숙(IS) 3.43 3.57 NaN 3.95 0.56 0.52 0.89 0.89
순자(EN) 1.12 NaN 0.59 0.43 4.01 4.16 3.52 3.38
현숙(EN) 0.94 1.05 0.32 0.45 4.02 3.78 NaN 3.54
서연(ES) 0.51 0.56 0.88 0.89 3.50 3.64 4.04 4.10
보람(ES) 0.48 0.51 1.03 NaN 3.52 4.00 3.82 NaN
하니(I) 4.85 4.82 NaN 4.98 4.53 4.39 4.45 4.52

- 안비슷한데 느낌만..

- 모델링

\[{\tt df\_view} \approx sig\left({\bf W}@{\bf M}^\top + bias \right) \times 5\]

- NAN 값 빼고 stack됨

df_train = df_view.stack().reset_index().set_axis(["여성출연자","남성출연자","궁합점수"],axis=1)
df_train
여성출연자 남성출연자 궁합점수
0 옥순(IN) 영철(IN) 4.02
1 옥순(IN) 영호(IS) 3.45
2 옥순(IN) 광수(IS) 3.42
3 옥순(IN) 상철(EN) 0.84
4 옥순(IN) 영수(EN) 1.12
... ... ... ...
58 하니(I) 광수(IS) 4.98
59 하니(I) 상철(EN) 4.53
60 하니(I) 영수(EN) 4.39
61 하니(I) 규빈(ES) 4.45
62 하니(I) 다호(ES) 4.52

63 rows × 3 columns

X1 = torch.nn.functional.one_hot(torch.tensor(df_train.여성출연자.map({name:i for i,name in enumerate(set(df_train.여성출연자))}))).float()
X2 = torch.nn.functional.one_hot(torch.tensor(df_train.남성출연자.map({name:i for i,name in enumerate(set(df_train.남성출연자))}))).float()
y = torch.tensor(df_train.궁합점수).float().reshape(-1,1)
X1.shape, X2.shape, y.shape
(torch.Size([63, 9]), torch.Size([63, 8]), torch.Size([63, 1]))
l1 = torch.nn.Linear(9,2,bias=False)
b1 = torch.nn.Linear(9,1, bias=False)
l2 = torch.nn.Linear(8,2,bias=False)
b2 = torch.nn.Linear(8,1,bias=False)
yhat = sig((l1(X1) * l2(X2)).sum(axis=1).reshape(-1,1)+b1(X1) + b2(X2))*5

C. 학습


여성인덱스 = {'옥순(IN)':0, '영자(IN)':1, '정숙(IS)':2, '영숙(IS)':3, '순자(EN)':4, '현숙(EN)':5, '서연(ES)':6, '보람(ES)':7, '하니(I)':8}
남성인덱스 = {'영식(IN)':0, '영철(IN)':1, '영호(IS)':2, '광수(IS)':3, '상철(EN)':4, '영수(EN)':5, '규빈(ES)':6, '다호(ES)':7}
df_view = pd.read_csv('https://raw.githubusercontent.com/guebin/DL2025/main/posts/iamsolo.csv',index_col=0)
df_train = df_view.stack().reset_index().set_axis(["여성출연자","남성출연자","궁합점수"],axis=1)
여성인덱스 = {'옥순(IN)':0, '영자(IN)':1, '정숙(IS)':2, '영숙(IS)':3, '순자(EN)':4, '현숙(EN)':5, '서연(ES)':6, '보람(ES)':7, '하니(I)':8}
남성인덱스 = {'영식(IN)':0, '영철(IN)':1, '영호(IS)':2, '광수(IS)':3, '상철(EN)':4, '영수(EN)':5, '규빈(ES)':6, '다호(ES)':7}
X1 = torch.nn.functional.one_hot(torch.tensor(df_train.여성출연자.map(여성인덱스))).float()
X2 = torch.nn.functional.one_hot(torch.tensor(df_train.남성출연자.map(남성인덱스))).float()
y = torch.tensor(df_train.궁합점수).float().reshape(-1,1)
#--#
torch.manual_seed(43052)
l1 = torch.nn.Linear(in_features=9, out_features=2, bias=False)
b1 = torch.nn.Linear(in_features=9, out_features=1, bias=False)
l2 = torch.nn.Linear(in_features=8, out_features=2, bias=False)
b2 = torch.nn.Linear(in_features=8, out_features=1, bias=False)
sig = torch.nn.Sigmoid()
loss_fn = torch.nn.MSELoss()
params = list(l1.parameters())+list(b1.parameters())+list(l2.parameters())+list(b2.parameters())
optimizr = torch.optim.Adam(params, lr=0.1) 
#--#
for epoc in range(100):
    ## step1 
    W_features = l1(X1)
    W_bias = b1(X1)
    M_features = l2(X2) 
    M_bias = b2(X2)
    yhat = sig((W_features * M_features).sum(axis=1).reshape(-1,1) + W_bias + M_bias) * 5 
    ## step2 
    loss = loss_fn(yhat,y)
    ## step3 
    loss.backward()
    ## step4 
    optimizr.step()
    optimizr.zero_grad()

- 무작위로 4개마다 예측값과 실제 궁합 비교

torch.concat([yhat,y],axis=1)[::4] # 꽤 잘맞음
tensor([[4.0679, 4.0200],
        [0.9554, 1.1200],
        [3.9950, 3.9900],
        [0.9580, 0.9600],
        [4.1490, 4.0500],
        [0.9765, 0.9900],
        [0.5392, 0.5600],
        [1.1028, 1.1200],
        [4.1009, 4.1600],
        [0.9950, 1.0500],
        [3.9835, 3.7800],
        [0.9398, 0.8800],
        [3.9916, 4.0400],
        [0.9030, 1.0300],
        [4.8949, 4.8500],
        [4.5048, 4.3900]], grad_fn=<SliceBackward0>)

D. 예측

df_view
영식(IN) 영철(IN) 영호(IS) 광수(IS) 상철(EN) 영수(EN) 규빈(ES) 다호(ES)
옥순(IN) NaN 4.02 3.45 3.42 0.84 1.12 0.43 0.49
영자(IN) 3.93 3.99 3.63 3.43 0.98 0.96 0.52 NaN
정숙(IS) 3.52 3.42 4.05 4.06 0.39 NaN 0.93 0.99
영숙(IS) 3.43 3.57 NaN 3.95 0.56 0.52 0.89 0.89
순자(EN) 1.12 NaN 0.59 0.43 4.01 4.16 3.52 3.38
현숙(EN) 0.94 1.05 0.32 0.45 4.02 3.78 NaN 3.54
서연(ES) 0.51 0.56 0.88 0.89 3.50 3.64 4.04 4.10
보람(ES) 0.48 0.51 1.03 NaN 3.52 4.00 3.82 NaN
하니(I) 4.85 4.82 NaN 4.98 4.53 4.39 4.45 4.52
df_train[:5]
여성출연자 남성출연자 궁합점수
0 옥순(IN) 영철(IN) 4.02
1 옥순(IN) 영호(IS) 3.45
2 옥순(IN) 광수(IS) 3.42
3 옥순(IN) 상철(EN) 0.84
4 옥순(IN) 영수(EN) 1.12
df_test = pd.DataFrame({'여성출연자':['옥순(IN)','하니(I)'],'남성출연자':['영식(IN)','영호(IS)']})
df_test
여성출연자 남성출연자
0 옥순(IN) 영식(IN)
1 하니(I) 영호(IS)
XX1 = torch.nn.functional.one_hot(torch.tensor(df_test.여성출연자.map(여성인덱스)),num_classes=9).float()
XX2 = torch.nn.functional.one_hot(torch.tensor(df_test.남성출연자.map(남성인덱스)),num_classes=8).float()
sig((l1(XX1) * l2(XX2)).sum(axis=1).reshape(-1,1) + b1(XX1) + b2(XX2))*5
tensor([[4.0671],
        [4.9121]], grad_fn=<MulBackward0>)