Skip to content
Snippets Groups Projects
Commit d6748054 authored by Jean-Luc Parouty's avatar Jean-Luc Parouty
Browse files

Rewrite Linear Reg. as jupyter example

parent c92e65ec
No related branches found
No related tags found
No related merge requests found
This diff is collapsed.
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
<img width="800px" src="../fidle/img/00-Fidle-header-01.svg"></img> <img width="800px" src="../fidle/img/00-Fidle-header-01.svg"></img>
# <!-- TITLE --> [FIT1] - Complexity Syndrome # <!-- TITLE --> [POLR1] - Complexity Syndrome
<!-- DESC --> Illustration of the problem of complexity with the polynomial regression <!-- DESC --> Illustration of the problem of complexity with the polynomial regression
<!-- AUTHOR : Jean-Luc Parouty (CNRS/SIMaP) --> <!-- AUTHOR : Jean-Luc Parouty (CNRS/SIMaP) -->
## Objectives : ## Objectives :
- Visualizing and understanding under and overfitting - Visualizing and understanding under and overfitting
## What we're going to do : ## What we're going to do :
We are looking for a polynomial function to approximate the observed series : We are looking for a polynomial function to approximate the observed series :
$ y = a_n\cdot x^n + \dots + a_i\cdot x^i + \dots + a_1\cdot x + b $ $ y = a_n\cdot x^n + \dots + a_i\cdot x^i + \dots + a_1\cdot x + b $
## Step 1 - Import and init ## Step 1 - Import and init
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
import numpy as np import numpy as np
import math import math
import random import random
import matplotlib import matplotlib
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import sys import sys
sys.path.append('..') sys.path.append('..')
import fidle.pwk as ooo import fidle.pwk as ooo
ooo.init() ooo.init()
``` ```
%% Output %% Output
FIDLE 2020 - Practical Work Module FIDLE 2020 - Practical Work Module
Version : 0.2.9 Version : 0.2.9
Run time : Tuesday 18 February 2020, 17:23:05 Run time : Tuesday 18 February 2020, 17:23:05
TensorFlow version : 2.0.0 TensorFlow version : 2.0.0
Keras version : 2.2.4-tf Keras version : 2.2.4-tf
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
## Step 2 - Preparation of learning data : ## Step 2 - Preparation of learning data :
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
# ---- Parameters # ---- Parameters
n = 100 n = 100
xob_min = -5 xob_min = -5
xob_max = 5 xob_max = 5
deg = 7 deg = 7
a_min = -2 a_min = -2
a_max = 2 a_max = 2
noise = 2000 noise = 2000
# ---- Train data # ---- Train data
# X,Y : data # X,Y : data
# X_norm,Y_norm : normalized data # X_norm,Y_norm : normalized data
X = np.random.uniform(xob_min,xob_max,(n,1)) X = np.random.uniform(xob_min,xob_max,(n,1))
# N = np.random.uniform(-noise,noise,(n,1)) # N = np.random.uniform(-noise,noise,(n,1))
N = noise * np.random.normal(0,1,(n,1)) N = noise * np.random.normal(0,1,(n,1))
a = np.random.uniform(a_min,a_max, (deg,)) a = np.random.uniform(a_min,a_max, (deg,))
fy = np.poly1d( a ) fy = np.poly1d( a )
Y = fy(X) + N Y = fy(X) + N
# ---- Data normalization # ---- Data normalization
# #
X_norm = (X - X.mean(axis=0)) / X.std(axis=0) X_norm = (X - X.mean(axis=0)) / X.std(axis=0)
Y_norm = (Y - Y.mean(axis=0)) / Y.std(axis=0) Y_norm = (Y - Y.mean(axis=0)) / Y.std(axis=0)
# ---- Data visualization # ---- Data visualization
width = 12 width = 12
height = 6 height = 6
nb_viz = min(2000,n) nb_viz = min(2000,n)
def vector_infos(name,V): def vector_infos(name,V):
m=V.mean(axis=0).item() m=V.mean(axis=0).item()
s=V.std(axis=0).item() s=V.std(axis=0).item()
print("{:8} : mean={:+12.4f} std={:+12.4f} min={:+12.4f} max={:+12.4f}".format(name,m,s,V.min(),V.max())) print("{:8} : mean={:+12.4f} std={:+12.4f} min={:+12.4f} max={:+12.4f}".format(name,m,s,V.min(),V.max()))
print("Nombre de points : {} a={} deg={} bruit={}".format(n,a,deg,noise)) print("Nombre de points : {} a={} deg={} bruit={}".format(n,a,deg,noise))
ooo.display_md('#### Before normalization :') ooo.display_md('#### Before normalization :')
print("\nDonnées d'aprentissage brute :") print("\nDonnées d'aprentissage brute :")
print("({} points visibles sur {})".format(nb_viz,n)) print("({} points visibles sur {})".format(nb_viz,n))
plt.figure(figsize=(width, height)) plt.figure(figsize=(width, height))
plt.plot(X[:nb_viz], Y[:nb_viz], '.') plt.plot(X[:nb_viz], Y[:nb_viz], '.')
plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False) plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
plt.xlabel('x axis') plt.xlabel('x axis')
plt.ylabel('y axis') plt.ylabel('y axis')
plt.show() plt.show()
vector_infos('X',X) vector_infos('X',X)
vector_infos('Y',Y) vector_infos('Y',Y)
ooo.display_md('#### After normalization :') ooo.display_md('#### After normalization :')
print("\nDonnées d'aprentissage normalisées :") print("\nDonnées d'aprentissage normalisées :")
print("({} points visibles sur {})".format(nb_viz,n)) print("({} points visibles sur {})".format(nb_viz,n))
plt.figure(figsize=(width, height)) plt.figure(figsize=(width, height))
plt.plot(X_norm[:nb_viz], Y_norm[:nb_viz], '.') plt.plot(X_norm[:nb_viz], Y_norm[:nb_viz], '.')
plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False) plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
plt.xlabel('x axis') plt.xlabel('x axis')
plt.ylabel('y axis') plt.ylabel('y axis')
plt.show() plt.show()
vector_infos('X_norm',X_norm) vector_infos('X_norm',X_norm)
vector_infos('Y_norm',Y_norm) vector_infos('Y_norm',Y_norm)
``` ```
%% Output %% Output
Nombre de points : 100 a=[-1.40023862 1.64009905 1.89987647 1.24972783 1.17765272 1.90935391 Nombre de points : 100 a=[-1.40023862 1.64009905 1.89987647 1.24972783 1.17765272 1.90935391
1.11259327] deg=7 bruit=2000 1.11259327] deg=7 bruit=2000
#### Before normalization : #### Before normalization :
Données d'aprentissage brute : Données d'aprentissage brute :
(100 points visibles sur 100) (100 points visibles sur 100)
X : mean= +0.2539 std= +2.9283 min= -4.9332 max= +4.9177 X : mean= +0.2539 std= +2.9283 min= -4.9332 max= +4.9177
Y : mean= -2914.8537 std= +5532.3607 min= -23848.6013 max= +5139.0627 Y : mean= -2914.8537 std= +5532.3607 min= -23848.6013 max= +5139.0627
#### After normalization : #### After normalization :
Données d'aprentissage normalisées : Données d'aprentissage normalisées :
(100 points visibles sur 100) (100 points visibles sur 100)
X_norm : mean= +0.0000 std= +1.0000 min= -1.7714 max= +1.5927 X_norm : mean= +0.0000 std= +1.0000 min= -1.7714 max= +1.5927
Y_norm : mean= -0.0000 std= +1.0000 min= -3.7839 max= +1.4558 Y_norm : mean= -0.0000 std= +1.0000 min= -3.7839 max= +1.4558
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
## Step 3 - Polynomial regression with NumPy ## Step 3 - Polynomial regression with NumPy
### 3.1 - Underfitting ### 3.1 - Underfitting
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
def draw_reg(X_norm, Y_norm, x_hat,fy_hat, size): def draw_reg(X_norm, Y_norm, x_hat,fy_hat, size):
plt.figure(figsize=size) plt.figure(figsize=size)
plt.plot(X_norm, Y_norm, '.') plt.plot(X_norm, Y_norm, '.')
x_hat = np.linspace(X_norm.min(), X_norm.max(), 100) x_hat = np.linspace(X_norm.min(), X_norm.max(), 100)
plt.plot(x_hat, fy_hat(x_hat)) plt.plot(x_hat, fy_hat(x_hat))
plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False) plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
plt.xlabel('x axis') plt.xlabel('x axis')
plt.ylabel('y axis') plt.ylabel('y axis')
plt.show() plt.show()
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
reg_deg=1 reg_deg=1
a_hat = np.polyfit(X_norm.reshape(-1,), Y_norm.reshape(-1,), reg_deg) a_hat = np.polyfit(X_norm.reshape(-1,), Y_norm.reshape(-1,), reg_deg)
fy_hat = np.poly1d( a_hat ) fy_hat = np.poly1d( a_hat )
print("Nombre de degrés : {} a_hat={}".format(reg_deg, a_hat)) print("Nombre de degrés : {} a_hat={}".format(reg_deg, a_hat))
draw_reg(X_norm[:nb_viz],Y_norm[:nb_viz], x_hat,fy_hat, (width,height)) draw_reg(X_norm[:nb_viz],Y_norm[:nb_viz], x_hat,fy_hat, (width,height))
``` ```
%% Output %% Output
Nombre de degrés : 1 a_hat=[ 2.15635737e-01 -1.26046371e-16] Nombre de degrés : 1 a_hat=[ 2.15635737e-01 -1.26046371e-16]
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### 3.2 - Good fitting ### 3.2 - Good fitting
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
reg_deg=5 reg_deg=5
a_hat = np.polyfit(X_norm.reshape(-1,), Y_norm.reshape(-1,), reg_deg) a_hat = np.polyfit(X_norm.reshape(-1,), Y_norm.reshape(-1,), reg_deg)
fy_hat = np.poly1d( a_hat ) fy_hat = np.poly1d( a_hat )
print("Nombre de degrés : {} a_hat={}".format(reg_deg, a_hat)) print("Nombre de degrés : {} a_hat={}".format(reg_deg, a_hat))
draw_reg(X_norm[:nb_viz],Y_norm[:nb_viz], x_hat,fy_hat, (width,height)) draw_reg(X_norm[:nb_viz],Y_norm[:nb_viz], x_hat,fy_hat, (width,height))
``` ```
%% Output %% Output
Nombre de degrés : 5 a_hat=[ 0.09676506 -0.49102546 -0.19674074 0.41539174 -0.00888844 0.51187173] Nombre de degrés : 5 a_hat=[ 0.09676506 -0.49102546 -0.19674074 0.41539174 -0.00888844 0.51187173]
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### 3.3 - Overfitting ### 3.3 - Overfitting
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` python ``` python
reg_deg=24 reg_deg=24
a_hat = np.polyfit(X_norm.reshape(-1,), Y_norm.reshape(-1,), reg_deg) a_hat = np.polyfit(X_norm.reshape(-1,), Y_norm.reshape(-1,), reg_deg)
fy_hat = np.poly1d( a_hat ) fy_hat = np.poly1d( a_hat )
print("Nombre de degrés : {} a_hat={}".format(reg_deg, a_hat)) print("Nombre de degrés : {} a_hat={}".format(reg_deg, a_hat))
draw_reg(X_norm[:nb_viz],Y_norm[:nb_viz], x_hat,fy_hat, (width,height)) draw_reg(X_norm[:nb_viz],Y_norm[:nb_viz], x_hat,fy_hat, (width,height))
``` ```
%% Output %% Output
Nombre de degrés : 24 a_hat=[-3.39583761e-01 3.66524802e+00 1.35968152e+01 -5.33709389e+01 Nombre de degrés : 24 a_hat=[-3.39583761e-01 3.66524802e+00 1.35968152e+01 -5.33709389e+01
-1.54708597e+02 3.43661072e+02 8.76139324e+02 -1.29075459e+03 -1.54708597e+02 3.43661072e+02 8.76139324e+02 -1.29075459e+03
-2.93637308e+03 3.13537269e+03 6.25956959e+03 -5.14495063e+03 -2.93637308e+03 3.13537269e+03 6.25956959e+03 -5.14495063e+03
-8.72124478e+03 5.75688179e+03 7.93008239e+03 -4.30734159e+03 -8.72124478e+03 5.75688179e+03 7.93008239e+03 -4.30734159e+03
-4.57884398e+03 2.04404969e+03 1.58025869e+03 -5.55090657e+02 -4.57884398e+03 2.04404969e+03 1.58025869e+03 -5.55090657e+02
-2.89374022e+02 7.05282858e+01 2.12619645e+01 -2.67799255e+00 -2.89374022e+02 7.05282858e+01 2.12619645e+01 -2.67799255e+00
3.95718335e-01] 3.95718335e-01]
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
--- ---
<img width="80px" src="../fidle/img/00-Fidle-logo-01.svg"></img> <img width="80px" src="../fidle/img/00-Fidle-logo-01.svg"></img>
......
# ------------------------------------------------------------------
# _____ _ _ _
# | ___(_) __| | | ___
# | |_ | |/ _` | |/ _ \
# | _| | | (_| | | __/
# |_| |_|\__,_|_|\___| Regression cooker
# ------------------------------------------------------------------
# Formation Introduction au Deep Learning (FIDLE)
# CNRS/SARI/DEVLOG 2020 - S. Arias, E. Maldonado, JL. Parouty
# ------------------------------------------------------------------
# Initial version by JL Parouty, feb 2020
import numpy as np
import math
import random
import datetime, time
import matplotlib
import matplotlib.pyplot as plt
from IPython.display import display,Markdown,HTML
class RegressionCooker():
__version__ = '0.1'
def __init__(self, mplstyle='../fidle/mplstyles/custom.mplstyle'):
print('\nFIDLE 2020 - Regression Cooker')
print('Version :', self.__version__)
print('Run time : {}'.format(time.strftime("%A %-d %B %Y, %H:%M:%S")))
if mplstyle is not None:
matplotlib.style.use(mplstyle)
@classmethod
def about(cls):
print('\nFIDLE 2020 - Regression Cooker)')
print('Version :', cls.version)
@classmethod
def vector_infos(cls,name,V):
"""
Show some nice infos about a vector
args:
name : vector name
V : vector
"""
m=V.mean(axis=0).item()
s=V.std(axis=0).item()
print("{:16} : mean={:8.3f} std={:8.3f} min={:8.3f} max={:8.3f}".format(name,m,s,V.min(),V.max()))
def get_dataset(self,n):
"""
Return a dataset of n observation
args:
n : dataset size
return:
X,Y : with X shapes = (n,1) Y shape = (n,)
"""
xob_min = 0 # x min and max
xob_max = 10
a_min = -30 # a min and max
a_max = 30
b_min = -10 # b min and max
b_max = 10
noise_min = 10 # noise min and max
noise_max = 50
a0 = random.randint(a_min,a_max)
b0 = random.randint(b_min,b_max)
noise = random.randint(noise_min,noise_max)
# ---- Construction du jeu d'apprentissage ---------------
# X,Y : données brutes
X = np.random.uniform(xob_min,xob_max,(n,1))
N = noise * np.random.normal(0,1,(n,1))
Y = a0*X + b0 + N
return X,Y
def plot_dataset(self,X,Y,title='Dataset :',width=12,height=6):
"""
Plot dataset X,Y
args:
X : Observations
Y : Values
"""
nb_viz = min(1000,len(X))
display(Markdown(f'### {title}'))
print(f"X shape : {X.shape} Y shape : {Y.shape} plot : {nb_viz} points")
plt.figure(figsize=(width, height))
plt.plot(X[:nb_viz], Y[:nb_viz], '.')
plt.show()
self.vector_infos('X',X)
self.vector_infos('Y',Y)
def __plot_theta(self, i, theta,x_min,x_max, loss,gradient,alpha):
Xd = np.array([[x_min], [x_max]])
Yd = Xd * theta.item(1) + theta.item(0)
plt.plot(Xd, Yd, color=(1.,0.4,0.3,alpha))
if i<0:
print( " #i Loss Gradient Theta")
else:
print(" {:3d} {:+7.3f} {:+7.3f} {:+7.3f} {:+7.3f} {:+7.3f}".format(i,loss,gradient.item(0),gradient.item(1),theta.item(0),theta.item(1)))
def __plot_XY(self, X,Y,width=12,height=6):
nb_viz = min(1000,len(X))
plt.figure(figsize=(width, height))
plt.plot(X[:nb_viz], Y[:nb_viz], '.')
plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
plt.xlabel('x axis')
plt.ylabel('y axis')
def __plot_loss(self,loss, width=8,height=4):
plt.figure(figsize=(width, height))
plt.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
plt.ylim(0, 20)
plt.plot(range(len(loss)), loss)
plt.xlabel('Iterations')
plt.ylabel('Loss')
def basic_descent(self, X, Y, epochs=200, eta=0.01,width=12,height=6):
"""
Performs a gradient descent where the gradient is updated at the end
of each iteration for all observations.
args:
X,Y : Observations
epochs : Number of epochs (200)
eta : learning rate
width,height : graphic size
return:
theta : theta
"""
display(Markdown(f'### Basic gradient descent :'))
display(Markdown(f'**With :** '))
print('with :')
print(f' epochs = {epochs}')
print(f' eta = {eta}')
display(Markdown(f'**epochs :** '))
x_min = X.min()
x_max = X.max()
y_min = Y.min()
y_max = Y.max()
n = len(X)
# ---- Initialization
theta = np.array([[y_min],[0]])
X_b = np.c_[np.ones((n, 1)), X]
# ---- Visualization
self.__plot_XY(X,Y,width,height)
self.__plot_theta( -1, theta,x_min,x_max, None,None,0.1)
# ---- Training
loss=[]
for i in range(epochs+1):
gradient = (2/n) * X_b.T @ ( X_b @ theta - Y)
mse = ((X_b @ theta - Y)**2).mean(axis=None)
theta = theta - eta * gradient
loss.append(mse)
if (i % (epochs/10))==0:
self.__plot_theta( i, theta,x_min,x_max, mse,gradient,i/epochs)
# ---- Visualization
display(Markdown(f'**Visualization :** '))
plt.show()
display(Markdown(f'**Loss :** '))
self.__plot_loss(loss)
plt.show()
return theta
def minibatch_descent(self, X, Y, epochs=200, batchs=5, batch_size=10, eta=0.01,width=12,height=6):
"""
Performs a gradient descent where the gradient is updated at the end
of each iteration for all observations.
args:
X,Y : Observations
epochs : Number of epochs (200)
eta : learning rate
width,height : graphic size
return:
theta : theta
"""
display(Markdown(f'### Mini batch gradient descent :'))
display(Markdown(f'**With :** '))
print('with :')
print(f' epochs = {epochs}')
print(f' batchs = {batchs}')
print(f' batch size = {batch_size}')
print(f' eta = {eta}')
display(Markdown(f'**epochs :** '))
x_min = X.min()
x_max = X.max()
y_min = Y.min()
y_max = Y.max()
n = len(X)
# ---- Initialization
theta = np.array([[y_min],[0]])
X_b = np.c_[np.ones((n, 1)), X]
# ---- Visualization
self.__plot_XY(X,Y,width,height)
self.__plot_theta( -1, theta,x_min,x_max, None,None,0.1)
# ---- Training
def learning_schedule(t):
return 1 / (t + 100)
loss=[]
for epoch in range(epochs):
for i in range(batchs):
random_index = np.random.randint(n-batch_size)
xi = X_b[random_index:random_index+batch_size]
yi = Y[random_index:random_index+batch_size]
mse = ((xi @ theta - yi)**2).mean(axis=None)
gradient = 2 * xi.T.dot(xi.dot(theta) - yi)
eta = learning_schedule(epoch*150)
theta = theta - eta * gradient
loss.append(mse)
self.__plot_theta( epoch, theta,x_min,x_max, mse,gradient,epoch/epochs)
# draw_theta(epoch,mse,gradients, theta,0.1+epoch/(n_epochs+1))
# draw_theta(epoch,mse,gradients,theta,1)
# ---- Visualization
display(Markdown(f'**Visualization :** '))
plt.show()
display(Markdown(f'**Loss :** '))
self.__plot_loss(loss)
plt.show()
return theta
\ No newline at end of file
...@@ -35,7 +35,7 @@ Useful information is also available in the [wiki](https://gricad-gitlab.univ-gr ...@@ -35,7 +35,7 @@ Useful information is also available in the [wiki](https://gricad-gitlab.univ-gr
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Direct determination of linear regression &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Direct determination of linear regression
[[GRAD1] - Linear regression with gradient descent](LinearReg/02-Gradient-descent.ipynb) [[GRAD1] - Linear regression with gradient descent](LinearReg/02-Gradient-descent.ipynb)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;An example of gradient descent in the simple case of a linear regression. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;An example of gradient descent in the simple case of a linear regression.
[[FIT1] - Complexity Syndrome](LinearReg/03-Polynomial-Regression.ipynb) [[POLR1] - Complexity Syndrome](LinearReg/03-Polynomial-Regression.ipynb)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Illustration of the problem of complexity with the polynomial regression &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Illustration of the problem of complexity with the polynomial regression
[[LOGR1] - Logistic regression, in pure Tensorflow](LinearReg/04-Logistic-Regression.ipynb) [[LOGR1] - Logistic regression, in pure Tensorflow](LinearReg/04-Logistic-Regression.ipynb)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Logistic Regression with Mini-Batch Gradient Descent using pure TensorFlow. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Logistic Regression with Mini-Batch Gradient Descent using pure TensorFlow.
......
...@@ -31,7 +31,7 @@ import seaborn as sn #IDRIS : module en cours d'installation ...@@ -31,7 +31,7 @@ import seaborn as sn #IDRIS : module en cours d'installation
from IPython.display import display,Markdown,HTML from IPython.display import display,Markdown,HTML
VERSION='0.2.9' VERSION='0.4.0'
# ------------------------------------------------------------- # -------------------------------------------------------------
...@@ -390,7 +390,11 @@ def good_place( places={'SOMEWHERE':'/tmp'} ): ...@@ -390,7 +390,11 @@ def good_place( places={'SOMEWHERE':'/tmp'} ):
print('** Attention : No expected folder exists in this environment..') print('** Attention : No expected folder exists in this environment..')
assert False, 'No expected folder exists in this environment..' assert False, 'No expected folder exists in this environment..'
def np_print(*args, format={'float': '{:6.3f}'.format}):
with np.printoptions(formatter=format):
for a in args:
print(a)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment