Tuesday, January 5, 2021

Create an Azure Document Queue for Loading and Tagging SharePoint Documents - Part 1

 

Why do we need a document queue?

SharePoint Online limitations


SharePoint is not known for being the most developer friendly platform, in fact the Stack Overflow survey indicates it being the most dreaded platform by developers https://insights.stackoverflow.com/survey/2018 . With so many companies moving to O365 and SharePoint online, developers have had even more struggles designing custom solutions on top of SharePoint.

One of the most frustrating of those is the dreaded 429 error, or when SharePoint throttles you. It is expected that using a software as a service product will come with some throttling. Microsoft does not want us bombarding their servers with requests, but there is no clear point on what throttles their system. This is a guess for all, and Microsoft determines this at random times. The best way to reduce throttling is to limit your calls. When dealing with list items this can be done by batching requests either using the REST or Graph API batch calls, but uploading documents does not have this luxury.  

Documents require several steps to upload a document with it's metadata which makes it very prone to throttling if you have several documents to upload. Looking at a typical CSOM call for a SharePoint upload we see about 5 calls  to upload and tag a document:

1. Call to get the list
2. Call to add document
3. Call to get the document after uploaded for list item properties
4. update the list item properties.
5. call to check in document

We can reduce these calls down to 3 using the REST API, but it still requires more calls than a list item. Uploading 1 or 2 documents at a time will not throttle your requests, but what if you need to migrate a shared drive, or you have an application that compiles refunds into a document that can be sent back to your customers? This is when throttling will begin. The solution: limit the number of calls to SharePoint. From experience, it seems 30 documents a minute is the maximum before you start to get throttled, this of course depends on how many total documents you are uploading. 30 documents once a day is fine, but if you are doing batches of 500, 30 documents continuously over a 20 minute period will start to throttle your user account. 

Getting performance out of my SharePoint app while limiting calls to SharePoint.


Knowing I need to make at least 3 calls a document how can I create an app that is performant but does not cause a system to throttle? The answer is Azure. Azure does have throttling limits but they are significantly higher thresholds than SharePoint. Using a storage account, we can create an intermediary between our application and SharePoint. Blob storage is fairly cheap in Azure. At the time of this writing, Azure charges for the average storage used not the total. What this means if  I am storing 2 GB worth of documents at a time I am charged for 2GB worth of storage. Even that means I load 2 GB's a day and remove it. 

A storage account also has several other features like creating a queue which can be used to hold our metadata. We can push and pop into the queue to reference our document,  and metadata by creating a simple JSON string. 

Once our documents are in Azure, we don't care how long it takes. Our users can continue on while we trickle load our documents into SharePoint using a web job.

Creating a SharePoint migration tool in 5 minutes - Part 1

Since our application is going to be split up into two parts, this post will be split up into two parts as well. Part one will cover creating a tool that will upload our documents to Azure. While Part 2 will cover the job that uploads and tags our documents in SharePoint.

The Data

Our data is simple, we are going to assume we have a file share that is storing our customer records. Our folder structure will be setup as so:

\\customers\{state}\{city}\{account number}

With this file folder setup I can extract my tags for state, city and account number for my documents.


Setup Azure Storage Account

If you do not have an Azure account please create a free one before continuing. To get started click Create Resource and search for "Storage Account". Create the resource and follow the prompts to create a new resource group, and storage account name. For the purposes of this blog, you can leave the rest as the default values.



Once our account is created we can see the options for blob and queues.



First click into containers, and at the top click "+ Container". We will name our container "customer". Repeat the same step for Queue but click "+ Queue" and name it "customer". Azure is now ready for us to start loading documents.

In order to authenticate to Azure, we will need a connection string. In the left hand navigation find Access Keys. Click into Access Keys and select "Show Keys" to expose your connection string. Once we have our connection string we can setup our application.




Creating the Project

For our migration tool, we will create 2 project. The first project covered in this blog is a console application that will run on our local machine and upload the documents to Azure. Our second project, will be a web job that runs in Azure and will continuously look for objects in the queue and try to upload them to SharePoint.

Let us start off by creating our first project called "SharePointMigration_Client". Once our console app is created we need to install 3 nuget packages (CTRL+ALT+P in VSCode). The 3 packages we need are:

  • Azure.Storage.Queues 
  • Azure.Storage.Blobs
  • Newtonsoft.JSON

The firsts two will give us references for accessing azure storage, and Newtonsoft is just a convienent way to serialize our object for the queue. This project will also require a reference System.Threading.Tasks to allow for async calls to Azure storage. Now that are project and references are created, we need to add a new class object to represent our SharePoint metadata. I called this file simply "CustomerMetadata.cs"


namespace SharePointMigration_Client
{
    public class CustomerMetadata
    {
        public string State {get; set;}
        public string City {get;set;}
        public string AccountNumber {get;set;}
        public string FileName {get;set;}
    }
}

Now that we have our customer object, we can define to properties; one for our queue, and another for our connection string to Azure. I define the queue as a property just so we can open the queue once, the blob container needs to be opened per document. The connection string is the same for our blob container and our queue so referecing it as property allows me to define it once and move on.


//making this a property so we do not have to keep recreating the object
static QueueClient queue; 
//connection string to our storage account, it is shared between blobs and queues.
static string cs = "Connection string from Azure";

The first function to define is our directory reader. This is a recursive function that will read all of the documents and sub directories found in our root directory "customers". When it finds a file it will upload it to Azure otherwise it will continue onto the next directory.


//Recurssive function for iterating directory and sub directories
public static async Task GetFilesInDirectory(string FileDirectory)
{
	Console.WriteLine("Looking for files in " + FileDirectory);
	string[] files = Directory.GetFiles(FileDirectory);

	foreach(string file in files)
	{
		Console.WriteLine(file);
		await UploadFileToAzureBlob(file);
		await UploadFileMetaDataToQueue(file);
	}

	string[] subdirectories = Directory.GetDirectories(FileDirectory);

	foreach(string subdirectory in subdirectories)
	{
		//Recursion for going thorugh all directories
		await GetFilesInDirectory(subdirectory);
	}
}

Once a file is found, we will need to upload the file to Azure. I prefer to upload the file before adding it to our queue since file uploads have more of a chance of breaking due to file size. The call to Azure is pretty straight forward:


public static async Task UploadFileToAzureBlob(string FilePath)
{
	string fileName = Path.GetFileName(FilePath);
	Console.WriteLine("File name {0}", fileName);
	//customer is the name of our blob container where we can view documents in Azure
	//blobs require us to create a connection each time we want to upload a file
	BlobClient blob  = new BlobClient(cs, "customer", fileName); 

	//Gets a file stream to upload to Azure
	using(FileStream stream = File.Open(FilePath, FileMode.Open))
	{
		await blob.UploadAsync(stream);
	}
}

Our file is now loaded into Azure, you can go back to your Azure storage account and view it in our customers container. Next we will upload our metadata. Since this is a five minute tutorial we can assume our directory is setup correctly and we do not need to parse or check our directory for the metadata. We will just split the directory by '\' and reference the position in the array. In a production environment, we would want to check to make sure our array is filled correctly. Also, note in this function I use "HTTPUtility.UrlEncode". I have found that Azure is picky about characters and can break if we do not encode the string. Specifically around non-UTC-8 characters.


public static async Task UploadFileMetaDataToQueue(string FilePath)
{
	string[] metadata = FilePath.Split('\\');

	//we know our metadata position because of our file structure. 
	CustomerMetadata customerMetadata = new CustomerMetadata()
	{
		State = metadata[2],
		City = metadata[3],
		AccountNumber = metadata[4],
		FileName = metadata[5]
	};

	//create a string for our queue
	string data = JsonConvert.SerializeObject(customerMetadata); 
	//We do this to ensure any non UTC-8 characters are safe for the web service
	data = HttpUtility.UrlEncode(data); 

	//adds message to back of the queue
	await queue.SendMessageAsync(data);
}

That's it, we just need to await our file directory sync and watch as files are switfly uploaded to Azure.


static async Task Main(string[] args)
{
	
	//create our queue client
	queue = new QueueClient(cs, "customer"); //customer is the name of our queue where we can view the items uploaded in Azure
	
	await GetFilesInDirectory("c:\\customers");
	Console.Read();
}

Results

After a successful run, you should see your files in your blob and metadata in your queue.

Continue to Part 2

In the next part, we will create a web job that reads the Azure Queue and uploads the documents to SharePoint.


Clone the project

You can find the full project on my GitHub site here https://github.com/fiveminutecoder/blogs/tree/master/SharePointMigration_Client




Azure, SharePoint, O365, SP, C#, Storage, Storage Account, Blob Storage, Azure Blob, Azure Blob Storage, Azure Storage Account, Azure Blob Storage Account, Azure Queue, Azure Queue Account, Queue, Serverless development, devleopment c sharp, 429, throttling, SharePoint Online, Office 365

Tuesday, December 8, 2020

Predicting Bitcoin Prices Using ML.Net and Time Series Techniques

Obligatory Machine Learning Stock Predictor 

It seems that anyone who starts to learn machine learning and analyzing big data thinks they can predict patterns in something as volatile as the stock market. Once you go down the rabbit hole it is easy to see that things are not random, we just need enough data so it is easy to see the attraction to trying to predicting stock prices.

Unfortunately, there are many external factors that data cannot predict. This was very clear when the 2020 pandemic hit, and stock prices tanked. Even so, trying is fun and helps us understand concepts. 

I am not a stock guru, and everything is this post is purely for learning purposes of ML.Net and how to use it for the time series function. Please use the code at your own risk for anything beyond learning how to create a time series model.

What is a Time Series Model?

Time Series in machine learning is trying to predict out over several periods. This is different from Regression which predicts the next period from a series. Some of the more common examples are housing prices, gas prices, and sales predictions. In big data analytics we can use linear regression to plot thousands of points and find the average over those points to create a line representing our answer. This is a good route, if you have two values, for example price/date you can create a nice graph that can represent sales.


Data prep

Before getting into the coding, data prep is key here. We are predicting items over time, so you want to know a time frame. Sales data for example, you usually compare year over year which is last year's sales vs this year's sales. We would use our time series model to predict the next 3 years sales or So you would break your data into yearly chunks usually daily prices. 


Create NLP FAQ Application in 5 Minutes

Data


With our Bitcoin example we will be breaking our data into daily changes in price with 1 minute increments. We will be using the btc.csv you can find here on my github page https://github.com/fiveminutecoder/blogs/blob/master/%20mlnet_BTCTimeSeries/btc.csv. *Update, it was found that this file was not consistent in it's time stamps so the code has been updated to use the following data set from Kaggle Bitcoin Historical Data. It is too large to add to the Git Hub site, this dataset was loaded by Zielak.

Our dataset has multiple columns, but when forecasting we are tracking a value over time, so only price will be used.

Creating the project

If you have not read my previous blog "Getting Started with ML.NET", which can be found here https://fiveminutecoder.blogspot.com/2020/07/getting-started-with-mlnet.html, please do so before continuing.

For this project we will continue to use the Microsoft.ML nuget package, but we will also need to add the Microsoft.ML.TimeSeries nuget package to get the forecasting estimator.

Once you have created a project and installed the Nuget packages, we can go ahead and create our data models to be used for our training data and our predictions. 

The first model is the BTCDataModel. It contains our pricing and timestamps we will use for training.


using Microsoft.ML.Data;

namespace mlnet_BTCTimeSeries
{
    public class BTCDataModel
    {
        [LoadColumn(0)]
        public int TimeStamp {get;set;}

        [LoadColumn(1)]
        public float Open {get;set;}
        [LoadColumn(2)]
        public float High {get;set;}
        [LoadColumn(3)]
        public float Low {get;set;}
        [LoadColumn(4)]
        public float Close {get;set;}

        [LoadColumn(5)]
        public float Volume {get;set;}
        [LoadColumn(6)]
        public float Currency {get;set;}

        [LoadColumn(7)]
        public float Amount {get;set;}
    }
}

Once we have created our training model, let's create our prediction model. The time series prediction model is slightly different than our supervised learning model. Instead of an array of confidence scores, we will have our prediction with an upper and lower bounds for accuracy.


using Microsoft.ML.Data;

namespace mlnet_BTCTimeSeries
{
    public class PredictedSeriesDataModel
    {
        public float[] ForecastedPrice { get; set; }
        public float[] ConfidenceLowerBound { get; set; }
        public float[] ConfidenceUpperBound { get; set; }
    }
}

Now that our models are setup, we can go ahead and add our context and training dataset place holders. We will set these globally so we can easily access them in our functions


using System;
using System.Linq;
using System.IO;
using Microsoft.ML;
using Microsoft.ML.Transforms.TimeSeries;
using System.Collections.Generic;

...

static MLContext mlContext = new MLContext();
//last time to show the times of our future predictions
static DateTime lastTime; 
static List trainingData;
static List testingData;
static string fileName = "btcModel.zip";
//how far out we want to predict
static int horizon = 5; 
//holds or in memory model
static ITransformer forecastTransformer; 

The first step or our project is to create our training and testing data, our testing data will be the size of our horizon field since that is the number we are trying to forecast, everything else will be used for training. We will also capture the last timestamp from the training model, this is used to show the next 5 predictions match the time from our test data.


static void GetTrainingData()
{
	//load our dataset
	IDataView trainingDataFile = mlContext.Data.LoadFromTextFile("bitstampUSD_1-min_data_2012-01-01_to_2020-12-31.csv", hasHeader: true, separatorChar: ',');

	//create enumerable to manipulate data
	List data = mlContext.Data.CreateEnumerable(trainingDataFile, false, true).ToList();

	//times in the data set are not uniform, so we will pull unique time values
	data = data.OrderBy( o => o.TimeStamp).ToList();

	//determines the size of our testing data
	int dataSubset = data.Count() - horizon;

	//create our training data up to the dates we are trying to predict
	trainingData = data.GetRange(0, dataSubset).ToList(); 

	// will get the number of items we are trying to predict
	testingData = data.GetRange(dataSubset, horizon);
	
	//We want to capture time of last item in training data so we can increment the time stamp for our output and put a date/time to the forecast
	lastTime = ConvertTimeStamp(trainingData.Last().TimeStamp);
}

//helper for converting timestamp to date time
static DateTime ConvertTimeStamp(double TimeStamp)
{
	var offset = TimeSpan.FromSeconds(TimeStamp);
	DateTime startTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
	return startTime.Add(offset).ToLocalTime();
}

With our data sets created, we can move on to creating our estimator. Unlike the supervised learning estimator, there is very little data manipulation. With the time series we just need to input the property names from our models to the appropriate columns. For this we will "nameof" to get our property names instead of labeling each property. There are a lot of settings in the forecasting estimator, the 3 main settings to worry about are "windowSize", "seriesLength", and "horizon". Window size is the periods on which our data is to reflect, series is the timespan of those windows, and finally horizon is the forecasting we want to produce or how far out we want to predict. 


static void TrainModel()
{
	IDataView trainingDataView = mlContext.Data.LoadFromEnumerable(trainingData);

	// creates our estimater, as you cans see we are using forecasting estimator
	var estimator = mlContext.Forecasting.ForecastBySsa(outputColumnName: nameof(PredictedSeriesDataModel.ForecastedPrice),
					inputColumnName: nameof(BTCDataModel.Amount), //column used for time series prediction
					windowSize: 60, //series is sampled in 60 minute windows or periods, and the past 60 minutes will be used to make the prediction
					seriesLength: 1440, //we want to train over a day's worth of time so this will be the interval, we have 1440 minutes in a day
					trainSize: trainingData.Count(), //how many data points we want to sample
					horizon: horizon,
					confidenceLevel: 0.45f, //sets our margin of error, lower the confidence level the smaller the upper/lower bounds
					confidenceLowerBoundColumn: nameof(PredictedSeriesDataModel.ConfidenceLowerBound),
					confidenceUpperBoundColumn: nameof(PredictedSeriesDataModel.ConfidenceUpperBound)
	);

	//creates our fitted model
	forecastTransformer = estimator.Fit(trainingDataView); 
}

One thing to notice in our trainer is that we are not calling save yet, this is will happen later in our predict function since time series data needs updating to stay relevant, we save a little differently. Our data is now fitted to our estimator, we can now predict our future Bitcoin prices.


static void Predict()
{
	//prediction engine based on our fitted model
	 TimeSeriesPredictionEngine forecastEngine = forecastTransformer.CreateTimeSeriesEngine(mlContext);

	//call to predict the next 5 minutes
	 PredictedSeriesDataModel predictions = forecastEngine.Predict();

	 //write our predictions
	 for(int i = 0; i < predictions.ForecastedPrice.Count(); i++)
	{   
		lastTime = lastTime.AddMinutes(1);
		Console.WriteLine("{0} price: {1}, low: {2}, high: {3}, actual: {4}", lastTime, predictions.ForecastedPrice[i].ToString(), predictions.ConfidenceLowerBound[i].ToString(), predictions.ConfidenceUpperBound[i].ToString(), testingData[i].Amount);
	}


	//instead of saving, we use checkpoint. This allows us to continue training with updated data and not need to keep such a large data set
	//so we can append Jan. 2021 without having everythign before to train the model speeding up the process
	forecastEngine.CheckPoint(mlContext, fileName);
}

The predict function is very similar to the rest of our predict functions, we visualize our data a bit different since we want to see all our our values not just the most accurate. At the end of this function we called "CheckPoint" on our forecastEngine. This creates a saved model that we can continue to train and add data points without retraining the entire model.


With our training and prediction functions complete, all that is left is calling them and viewing our results.


static void Main(string[] args)
{
	GetTrainingData();
	TrainModel();
	Predict();
}

Results

As we can see, the forecaster is fairly accurate predicting the next minute's price (off by about $5) but unfortunately our model predicted a downward tend instead of an upward one as we see comparing to our actual results. This just shows price history alone is not enough to predict the stock market.



Clone the project


You can find the full project on my GitHub site here https://github.com/fiveminutecoder/blogs/tree/master/%20mlnet_BTCTimeSeries
AI, Artificial Intelligence, BTC, BitCoin, C#, Time Series, dotnet, dot net, machine learning, mldotnet, ml.net, dotnet core, dotnet 5, .NET 5

Tuesday, November 10, 2020

Using ML.NET for Natural Language Processing (NLP) in 5 minutes

 What is Natural Language Processing?

Natural language processing, or NLP, is taking text and and converting it to something your application can use. What we are expecting is for someone to type in a word or sentence and the application is able to understand and process the command. The challenge here is not everyone communicates the same way for example:

  • Please save document.
  • Save document.
  • Update Document.
  • I need my document to be put into my accounting folder.

All can be interpreted as a "Save" command. This is a great task for machine learning. We can train our algorithm to interpret what the user is trying to communicate, and complete the task. If you are not familiar with the basics of ML.NET or supervised learning, please check out my previous post https://fiveminutecoder.blogspot.com/2020/07/getting-started-with-mlnet.html.

Create NLP FAQ application in 5 minutes

The Data

I have created a basic csv file with several FAQ questions and answers for a fictional business, you can download the file here which is part of the GitHub repository https://github.com/fiveminutecoder/blogs/tree/master/mlnet_NLP.

Creating the project

If you have not read my previous blog "Getting Started with ML.NET", which can be found here https://fiveminutecoder.blogspot.com/2020/07/getting-started-with-mlnet.html please do so before continuing since we will be referencing back to it frequently. Following our previous example, we will create a new console application called "mlnet_NLP". Once the project is created we will download the "Microsoft.ML" package from Nuget.

Once you have the project setup we will need to create our two data models, one for the input features of the FAQ (our question and answer), and one for our predictions.


	public class FAQModel
	{
		[ColumnName("Question"), LoadColumn(0)]
		public string Question {get;set;}
		[ColumnName("Answer"), LoadColumn(1)]
		public string Answer {get;set;}
	}



	public class PredictionModel
	{
		[ColumnName("PredictedAnswer")]
		public string PredictedAnswer {get;set;}
		[ColumnName("Score")]
		public float[] Score {get;set;}
	}


Once we have our data models setup, we need to setup our program. We will use the same 5 functions from our pervious example which will train, test, predict, save, and load our machine learning model. Before we begin, we need to add our references and fields that will hold our context and data. Again, this should be a separate class, but for the sake of time we will do all this in our program.cs.


	//Main context
	static MLContext context;
	//model for training/testing
	static Microsoft.ML.Data.TransformerChain model;
	static IEnumerable trainingData;
	static IEnumerable testingData;
	static string fileName = "FAQModel.zip";

Our training function will be very similar to the previous one, with the exception to how to create our features. Instead of our features being several columns, we have 1 column with several words. Luckily ML.Net has a function that lets us featurize text making it a quick swap of our previous concatenate features line. The FeaturizeText method is very powerful and performs several operations under the hood, like remove stop words like the, and, or, etc. To learn more, visit Microsoft's documentation around preparing data https://docs.microsoft.com/en-us/dotnet/machine-learning/how-to-guides/prepare-data-ml-net


	static void TrainModel()
	{
		context = new MLContext();

		//Load data from csv file
		var data = context.Data.LoadFromTextFile("faq.csv", hasHeader:true, separatorChar: ',', allowQuoting: true, allowSparse:true, trimWhitespace: true);
		

		//create data sets for trainiing and testing
		trainingData = context.Data.CreateEnumerable(data, reuseRowObject: false);
		testingData = new List()
		{
			new FAQModel() {Question = "When are you open?", Answer = "Our hours are 9 am to 5pm Monday through Friday"},
			new FAQModel() {Question = "Can i pay using a visa card?", Answer =  "Our payment options are Credit, Check, or Bitcoin"},
			new FAQModel() {Question = "How can i contact you.", Answer = "Our phone number is 555-5555 and our fax is 555-5557"}
		};

		//Create our pipeline and set our training model
		var pipeline = context.Transforms.Conversion.MapValueToKey(outputColumnName: "Label", inputColumnName: "Answer") //converts string to key value for training
			.Append(context.Transforms.Text.FeaturizeText( "Features","Question")) //creates features from our text string
			.Append(context.Transforms.Text.f)
			.Append(context.MulticlassClassification.Trainers.SdcaMaximumEntropy(labelColumnName: "Label", featureColumnName: "Features"))//set up our model
			.Append(context.Transforms.Conversion.MapKeyToValue(outputColumnName: "PredictedAnswer", inputColumnName: "PredictedLabel")); //convert our key back to a label

		//traings the model
		 model = pipeline.Fit(context.Data.LoadFromEnumerable(trainingData));
	}

Now that our training method is setup, we want to test our model. This FAQ is too small to break up, so the accuracy will return as 0. To remedy this, I  manually added a couple tests to our enumerable.


	static void TestModel()
	{
		//transform data to a view that can be evaluated
		IDataView testDataPredictions = model.Transform(context.Data.LoadFromEnumerable(testingData));
		//evaluate test data against trained model for accuracy
		var metrics = context.MulticlassClassification.Evaluate(testDataPredictions);
		double accuracy = metrics.MacroAccuracy;

		Console.WriteLine("Accuracy {0}", accuracy.ToString());
	}

Now that our model is trained, we will save it so it can be loaded in our prediction engine. 


	static void SaveModel()
	{
		IDataView dataView = context.Data.LoadFromEnumerable(trainingData);
	       context.Model.Save(model, dataView.Schema, fileName);
	}



	static ITransformer LoadModel()
	{
		DataViewSchema modelSchema;
		//gets a file from a stream, and loads it
		using(Stream s = File.Open(fileName, FileMode.Open))
		{
			return context.Model.Load(s, out modelSchema);
		}
	}

Now we can setup our prediction engine. Again this is exactly how we set it up in the previous example. Our NLP uses a multiclass supervised learning model so predicting our answer is handled the same; pass our question in, and the machine learning algorithm will spit out an answer.


	static void Predict(FAQModel Question)
	{
		ITransformer trainedModel = LoadModel();

		//Creates prediction function from loaded model, you can load in memory model as wwell
		 var predictFunction = context.Model.CreatePredictionEngine(trainedModel);
		 
		//pass model to function to get prediction outputs
		PredictionModel prediction = predictFunction.Predict(Question);

		//get score, score is an array and the max score will align to key.
		float score = prediction.Score.Max();
	
		Console.WriteLine("Prediction: {0},  accuracy: {1}", prediction.PredictedAnswer, score);

	}

Now that we have our functions setup, we can call them in the static main function and start answering questions.


	static void Main(string[] args)
	{
		TrainModel();
		TestModel();
		SaveModel();
		FAQModel question = new FAQModel(){
			Question = "can i Pay online?",
			Answer = ""
		};

		Predict(question);
	}

Clone the project


you can find the full project on my GitHub site here https://github.com/fiveminutecoder/blogs/tree/master/mlnet_NLP
AI, Artificial Intelligence, C#, NLP, Natural Language Processing, supervised learning, dotnet, dot net, machine learning, mldotnet, ml.net, dotnet core, dotnet 5, .NET 5
C#, C sharp, machine learning, ML.NET, dotnet core, dotnet, O365, Office 365, developer, development, Azure, Supervised Learning, Unsupervised Learning, NLP, Natural Language Programming, Microsoft, SharePoint, Teams, custom software development, sharepoint specialist, chat GPT,artificial intelligence, AI

Cookie Alert

This blog was created and hosted using Google's platform Blogspot (blogger.com). In accordance to privacy policy and GDPR please note the following: Third party vendors, including Google, use cookies to serve ads based on a user's prior visits to your website or other websites. Google's use of advertising cookies enables it and its partners to serve ads to your users based on their visit to your sites and/or other sites on the Internet. Users may opt out of personalized advertising by visiting Ads Settings. (Alternatively, you can opt out of a third-party vendor's use of cookies for personalized advertising by visiting www.aboutads.info.) Google analytics is also used, for more details please refer to Google Analytics privacy policy here: Google Analytics Privacy Policy Any information collected or given during sign up or sign is through Google's blogger platform and is stored by Google. The only Information collected outside of Google's platform is consent that the site uses cookies.